diff --git a/.gitignore b/.gitignore
index 40b878d..a9cc52e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
-node_modules/
\ No newline at end of file
+node_modules/
+.waylog/
+**/.audit/
+.DS_Store
+design-qa.md
+**/design-qa.md
diff --git a/AGENTS.md b/AGENTS.md
index 8fe20bd..d9af99b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,3 +11,11 @@ H5 多语言能力放在 common/i18n.js 和 common/locales/\*.json,当前面
所有逻辑功能配置默认打开;只有密钥、地址、三方账号等必须由环境提供的敏感值使用占位符或环境配置,不在默认配置里伪造可用凭证。
所有业务逻辑、状态流转、权限判断、支付链路、异步流程都保持高密度逻辑注释,注释解释为什么这样处理和失败分支,不复述语法。
+
+## Figma / 截图还原超级红线
+
+- 全局永久禁止把整页截图、Figma 根 Frame、整段轮播、整块底栏或包含多个可独立元素的大区块直接作为页面图片实现。
+- 设计稿必须拆成独立视觉图层;文字、按钮、状态、列表、动态媒体与交互区域必须使用 DOM、CSS 和真实接口数据实现。
+- 导出的设计素材必须能追溯到具体的单一视觉节点,并记录 source node、用途、尺寸和哈希;禁止素材内嵌动态文字、控件或业务媒体。
+- 允许使用不包含文字、控件和动态业务内容的纯背景层,但它不能替代其上的任何可拆元素。
+- 验收必须按图层、元素边界、间距、样式和交互逐项核对;禁止用整页设计截图覆盖页面,也禁止把整页截图像素差分作为通过依据。
diff --git a/activity/cp-space/fami.html b/activity/cp-space/fami.html
new file mode 100644
index 0000000..5f861ba
--- /dev/null
+++ b/activity/cp-space/fami.html
@@ -0,0 +1,267 @@
+
+
+
+
+
+ Best Friend
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+ Best Friend
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
0
+
Days
+
+
+
+
+
+
+

+
+
+
+
+ 0/0
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/cp-space/fami/assets/fonts/OFL.txt b/activity/cp-space/fami/assets/fonts/OFL.txt
new file mode 100644
index 0000000..3ff0cca
--- /dev/null
+++ b/activity/cp-space/fami/assets/fonts/OFL.txt
@@ -0,0 +1,96 @@
+Copyright 2014-2025 Adobe (http://www.adobe.com/), with Reserved Font
+Name 'Source'. Source is a trademark of Adobe in the United States
+and/or other countries.
+
+This Font Software is licensed under the SIL Open Font License,
+Version 1.1.
+
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font
+creation efforts of academic and linguistic communities, and to
+provide a free and open framework in which fonts may be shared and
+improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply to
+any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software
+components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to,
+deleting, or substituting -- in part or in whole -- any of the
+components of the Original Version, by changing formats or by porting
+the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed,
+modify, redistribute, and sell modified and unmodified copies of the
+Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in
+Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the
+corresponding Copyright Holder. This restriction only applies to the
+primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created using
+the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/activity/cp-space/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2 b/activity/cp-space/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2
new file mode 100644
index 0000000..8519f51
Binary files /dev/null and b/activity/cp-space/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2 differ
diff --git a/activity/cp-space/fami/assets/layers/avatar-frame.png b/activity/cp-space/fami/assets/layers/avatar-frame.png
new file mode 100644
index 0000000..9a7249f
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/avatar-frame.png differ
diff --git a/activity/cp-space/fami/assets/layers/back.svg b/activity/cp-space/fami/assets/layers/back.svg
new file mode 100644
index 0000000..d2935f3
--- /dev/null
+++ b/activity/cp-space/fami/assets/layers/back.svg
@@ -0,0 +1,9 @@
+
diff --git a/activity/cp-space/fami/assets/layers/battery.svg b/activity/cp-space/fami/assets/layers/battery.svg
new file mode 100644
index 0000000..15c113d
--- /dev/null
+++ b/activity/cp-space/fami/assets/layers/battery.svg
@@ -0,0 +1,7 @@
+
diff --git a/activity/cp-space/fami/assets/layers/bg-brother.png b/activity/cp-space/fami/assets/layers/bg-brother.png
new file mode 100644
index 0000000..7a471d7
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/bg-brother.png differ
diff --git a/activity/cp-space/fami/assets/layers/bg-cp.png b/activity/cp-space/fami/assets/layers/bg-cp.png
new file mode 100644
index 0000000..2d74a9f
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/bg-cp.png differ
diff --git a/activity/cp-space/fami/assets/layers/bg-sister.png b/activity/cp-space/fami/assets/layers/bg-sister.png
new file mode 100644
index 0000000..e6c18d9
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/bg-sister.png differ
diff --git a/activity/cp-space/fami/assets/layers/card-brother.png b/activity/cp-space/fami/assets/layers/card-brother.png
new file mode 100644
index 0000000..230c1fb
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/card-brother.png differ
diff --git a/activity/cp-space/fami/assets/layers/card-cp.png b/activity/cp-space/fami/assets/layers/card-cp.png
new file mode 100644
index 0000000..efb405e
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/card-cp.png differ
diff --git a/activity/cp-space/fami/assets/layers/card-sister.png b/activity/cp-space/fami/assets/layers/card-sister.png
new file mode 100644
index 0000000..1297f16
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/card-sister.png differ
diff --git a/activity/cp-space/fami/assets/layers/cellular.svg b/activity/cp-space/fami/assets/layers/cellular.svg
new file mode 100644
index 0000000..a58f453
--- /dev/null
+++ b/activity/cp-space/fami/assets/layers/cellular.svg
@@ -0,0 +1,3 @@
+
diff --git a/activity/cp-space/fami/assets/layers/center-brother.png b/activity/cp-space/fami/assets/layers/center-brother.png
new file mode 100644
index 0000000..4f9f2d6
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/center-brother.png differ
diff --git a/activity/cp-space/fami/assets/layers/center-cp.png b/activity/cp-space/fami/assets/layers/center-cp.png
new file mode 100644
index 0000000..0e3c25a
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/center-cp.png differ
diff --git a/activity/cp-space/fami/assets/layers/center-sister.png b/activity/cp-space/fami/assets/layers/center-sister.png
new file mode 100644
index 0000000..78ceef0
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/center-sister.png differ
diff --git a/activity/cp-space/fami/assets/layers/help.svg b/activity/cp-space/fami/assets/layers/help.svg
new file mode 100644
index 0000000..ca4c465
--- /dev/null
+++ b/activity/cp-space/fami/assets/layers/help.svg
@@ -0,0 +1,5 @@
+
diff --git a/activity/cp-space/fami/assets/layers/level-brother.png b/activity/cp-space/fami/assets/layers/level-brother.png
new file mode 100644
index 0000000..b8688b8
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/level-brother.png differ
diff --git a/activity/cp-space/fami/assets/layers/level-cp.png b/activity/cp-space/fami/assets/layers/level-cp.png
new file mode 100644
index 0000000..12570da
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/level-cp.png differ
diff --git a/activity/cp-space/fami/assets/layers/level-sister.png b/activity/cp-space/fami/assets/layers/level-sister.png
new file mode 100644
index 0000000..269dd83
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/level-sister.png differ
diff --git a/activity/cp-space/fami/assets/layers/manifest.json b/activity/cp-space/fami/assets/layers/manifest.json
new file mode 100644
index 0000000..5b4876e
--- /dev/null
+++ b/activity/cp-space/fami/assets/layers/manifest.json
@@ -0,0 +1,464 @@
+{
+ "schemaVersion": 1,
+ "rule": "Layered restoration only. Root frames, full-page screenshots, dynamic text, controls, and production avatars are not implementation assets.",
+ "figma": {
+ "fileKey": "vaDp5wwn6AOQVlt9zk0f9r",
+ "page": {
+ "id": "1:27538",
+ "name": "yumi"
+ },
+ "entryFrames": {
+ "cp": "859:2027",
+ "brother": "859:2247",
+ "sister": "859:2454"
+ }
+ },
+ "baseFrame": {
+ "width": 375,
+ "height": 812
+ },
+ "assets": [
+ {
+ "file": "bg-cp.png",
+ "sourceNode": "859:2028",
+ "equivalentNodes": [],
+ "sourceName": "Mask group",
+ "sourceType": "frame",
+ "purpose": "CP pure background layer",
+ "bounds": { "x": 0, "y": 0, "width": 375, "height": 812 },
+ "exportFormat": "png",
+ "bytes": 685603,
+ "sha256": "a16b0d609a0485653e866b6f6cdaa22a8e22cf0d4bd2edd6c738d6e1b8e82747",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "bg-brother.png",
+ "sourceNode": "859:2248",
+ "equivalentNodes": [],
+ "sourceName": "Mask group",
+ "sourceType": "frame",
+ "purpose": "Brother pure background layer",
+ "bounds": { "x": 0, "y": 0, "width": 375, "height": 812 },
+ "exportFormat": "png",
+ "bytes": 693082,
+ "sha256": "866c6b111bd91894dab45d23a96493cb83fb526e282ce9ce26a4f243f5896c5e",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "bg-sister.png",
+ "sourceNode": "859:2455",
+ "equivalentNodes": [],
+ "sourceName": "Mask group",
+ "sourceType": "frame",
+ "purpose": "Sister pure background layer",
+ "bounds": { "x": 0, "y": 0, "width": 375, "height": 812 },
+ "exportFormat": "png",
+ "bytes": 718426,
+ "sha256": "701a6735476e4fe44c2c01a94968dc6704c94c72171f788bdf9be398dacbd554",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "tab-cp.png",
+ "sourceNode": "859:2059",
+ "equivalentNodes": [],
+ "sourceName": "Group 2090056311",
+ "sourceType": "frame",
+ "purpose": "Selected CP tab jewel without label text",
+ "bounds": { "x": 9, "y": 107, "width": 130, "height": 48 },
+ "exportFormat": "png",
+ "bytes": 12416,
+ "sha256": "2f024464ea3b42311732118312817f92af4558e2abf73331b96951515f961de0",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "tab-brother.png",
+ "sourceNode": "859:2279",
+ "equivalentNodes": [],
+ "sourceName": "Group 2090056311",
+ "sourceType": "frame",
+ "purpose": "Selected Brother tab jewel without label text",
+ "bounds": { "x": 126, "y": 107, "width": 130, "height": 48 },
+ "exportFormat": "png",
+ "bytes": 12330,
+ "sha256": "eb092fc233d2496860f8be984bb259da4c32357703edd3f4dec51517ea7ba66c",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "tab-sister.png",
+ "sourceNode": "859:2486",
+ "equivalentNodes": [],
+ "sourceName": "Group 2090056343",
+ "sourceType": "frame",
+ "purpose": "Selected Sister tab jewel without label text",
+ "bounds": { "x": 236, "y": 107, "width": 130, "height": 48 },
+ "exportFormat": "png",
+ "bytes": 11938,
+ "sha256": "ed44cc60056760e216131dda2b932783a315de9c6ba88992fbafb6d210948f76",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "card-cp.png",
+ "sourceNode": "859:2065",
+ "equivalentNodes": [],
+ "sourceName": "Group 2090056340",
+ "sourceType": "frame",
+ "purpose": "CP decorative card skin without relationship data",
+ "bounds": { "x": 14, "y": 169, "width": 347, "height": 199 },
+ "exportFormat": "png",
+ "bytes": 117918,
+ "sha256": "4f78f7349e7bdd2d501d7fd017a1fe955323d6d9d316b818f4d72b1645c84929",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "card-brother.png",
+ "sourceNode": "859:2287",
+ "equivalentNodes": [],
+ "sourceName": "Group 2090056325",
+ "sourceType": "frame",
+ "purpose": "Brother decorative card skin without relationship data",
+ "bounds": { "x": 14, "y": 169, "width": 347, "height": 199 },
+ "exportFormat": "png",
+ "bytes": 123692,
+ "sha256": "81e6bbf780fbccb7796c160d4e8e6596bed2af6cfec0a2b378f8ebf1fd466e9b",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "card-sister.png",
+ "sourceNode": "859:2494",
+ "equivalentNodes": [],
+ "sourceName": "Group 2090056336",
+ "sourceType": "frame",
+ "purpose": "Sister decorative card skin without relationship data",
+ "bounds": { "x": 14, "y": 169, "width": 347, "height": 199 },
+ "exportFormat": "png",
+ "bytes": 120470,
+ "sha256": "803644ee833188033a2c4f7a2b4f051228ca7138eb0d478e426a24056a461653",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "center-cp.png",
+ "sourceNode": "859:2189",
+ "equivalentNodes": [],
+ "sourceName": "image 4790",
+ "sourceType": "rounded-rectangle",
+ "purpose": "CP center heart ornament without days or EXP text",
+ "bounds": { "x": 120, "y": 175, "width": 136, "height": 120 },
+ "exportFormat": "png",
+ "bytes": 20823,
+ "sha256": "5041a01b2856d7f28843601652da21791d47b81a6d0a22b08a80f83c487c5520",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "center-brother.png",
+ "sourceNode": "859:2409",
+ "equivalentNodes": [],
+ "sourceName": "image 4799",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Brother center shield ornament without days or EXP text",
+ "bounds": { "x": 120, "y": 175, "width": 136, "height": 136 },
+ "exportFormat": "png",
+ "bytes": 18728,
+ "sha256": "1765558831f54c2404464d5ae907ddf0011d094eecb8c927b5be47af53d8ee62",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "center-sister.png",
+ "sourceNode": "859:2618",
+ "equivalentNodes": [],
+ "sourceName": "image 4796",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Sister floral ornament without days or EXP text",
+ "bounds": { "x": 120, "y": 175, "width": 136, "height": 136 },
+ "exportFormat": "png",
+ "bytes": 34323,
+ "sha256": "eccd16b84a61a262953eac81cd5b511256bfb595c9c3a130785c5861dfb2fd43",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "avatar-frame.png",
+ "sourceNode": "859:2183",
+ "equivalentNodes": [
+ "859:2211",
+ "859:2403",
+ "859:2418",
+ "859:2612",
+ "859:2627"
+ ],
+ "sourceName": "3 4456240",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Decorative avatar ring shared by both relationship members",
+ "bounds": { "width": 66, "height": 66 },
+ "exportFormat": "png",
+ "bytes": 4406,
+ "sha256": "a2d56dd2b81fafa471ca6a41f7f59df9d8aba2c29c9e90f5348d6818b4326e30",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "level-cp.png",
+ "sourceNode": "859:2218",
+ "equivalentNodes": ["859:2228"],
+ "sourceName": "image 4816",
+ "sourceType": "rounded-rectangle",
+ "purpose": "CP level badge background without level text",
+ "bounds": { "width": 24, "height": 13 },
+ "exportFormat": "png",
+ "bytes": 1016,
+ "sha256": "36e0a7bbcf4cb5d39bd61153b8ca1cf4e76e8c3293c064127e5c097bf7a5d148",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "level-brother.png",
+ "sourceNode": "859:2425",
+ "equivalentNodes": ["859:2435"],
+ "sourceName": "image 4817",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Brother level badge background without level text",
+ "bounds": { "width": 24, "height": 13 },
+ "exportFormat": "png",
+ "bytes": 1017,
+ "sha256": "da0b413aab96eb7890af30f9a3daef16c2ea03d607767430f2b8c6fb483b4b4b",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "level-sister.png",
+ "sourceNode": "859:2634",
+ "equivalentNodes": ["859:2644"],
+ "sourceName": "image 4819",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Sister level badge background without level text",
+ "bounds": { "width": 24, "height": 13 },
+ "exportFormat": "png",
+ "bytes": 967,
+ "sha256": "ac5339c0505f194757e1ae0f67e3e69633f73a8c19f99355b956c0d1b038024c",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "pill-cp-left.png",
+ "sourceNode": "859:2191",
+ "equivalentNodes": [],
+ "sourceName": "Group 2085668693 2",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Left CP EXP pill cap without dynamic text",
+ "bounds": { "width": 49, "height": 28 },
+ "exportFormat": "png",
+ "bytes": 2196,
+ "sha256": "01a8328f9e1f2ebc0973f5cdecb104af634bb28c5d47f5b75d78d0837d7bafdd",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "pill-cp-center.png",
+ "sourceNode": "859:2192",
+ "equivalentNodes": [
+ "859:2193",
+ "859:2194",
+ "859:2195",
+ "859:2196",
+ "859:2197",
+ "859:2198",
+ "859:2199",
+ "859:2200",
+ "859:2201",
+ "859:2202",
+ "859:2203",
+ "859:2204",
+ "859:2205"
+ ],
+ "sourceName": "Group 2085668693 4",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Repeatable CP EXP pill center slice without dynamic text",
+ "bounds": { "width": 1, "height": 28 },
+ "exportFormat": "png",
+ "bytes": 241,
+ "sha256": "e9940fdbe009e5db2c38992609d56bc162262da870bb5032cc08218aa979444c",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "pill-cp-right.png",
+ "sourceNode": "859:2206",
+ "equivalentNodes": [],
+ "sourceName": "Group 2085668693 3",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Right CP EXP pill cap without dynamic text",
+ "bounds": { "width": 49, "height": 28 },
+ "exportFormat": "png",
+ "bytes": 2236,
+ "sha256": "1ad302ea0c693b0544e666c0a0289a42eaf27cd1a3b210040cf1fe7fc839fa9d",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "pill-brother.png",
+ "sourceNode": "859:2411",
+ "equivalentNodes": [],
+ "sourceName": "Mask group",
+ "sourceType": "frame",
+ "purpose": "Brother EXP pill background without dynamic text",
+ "bounds": { "width": 112, "height": 28 },
+ "exportFormat": "png",
+ "bytes": 5154,
+ "sha256": "90af1c3725f500489f84e25b1a025aa20ca6bc6edab56087f108a32dfeecccfc",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "pill-sister.png",
+ "sourceNode": "859:2620",
+ "equivalentNodes": [],
+ "sourceName": "Mask group",
+ "sourceType": "frame",
+ "purpose": "Sister EXP pill background without dynamic text",
+ "bounds": { "width": 112, "height": 28 },
+ "exportFormat": "png",
+ "bytes": 5095,
+ "sha256": "b7587a4b7b2e1c40bbb9cc42689349351f55b933ab0a687d4ea4a4e2c65440d9",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "back.svg",
+ "sourceNode": "859:2051",
+ "equivalentNodes": ["859:2271", "859:2478"],
+ "sourceName": "icon_20_返回",
+ "sourceType": "frame",
+ "purpose": "Back glyph rendered inside a real DOM button",
+ "bounds": { "x": 16, "y": 54, "width": 24, "height": 24 },
+ "exportFormat": "svg",
+ "bytes": 419,
+ "sha256": "50d06f24adb0019fcdfb3c3f5db012808c230cd8c2f06c61b775fcccdbf525d4",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "help.svg",
+ "sourceNode": "859:2055",
+ "equivalentNodes": ["859:2275", "859:2482"],
+ "sourceName": "help",
+ "sourceType": "frame",
+ "purpose": "Help glyph rendered inside a real DOM button",
+ "bounds": { "x": 335, "y": 54, "width": 24, "height": 24 },
+ "exportFormat": "svg",
+ "bytes": 1535,
+ "sha256": "18087727ea208b257df9d875523dd45e29ccd0b05912a4504f72555cd15c045e",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "battery.svg",
+ "sourceNode": "859:2034",
+ "equivalentNodes": ["859:2254", "859:2461"],
+ "sourceName": "Battery",
+ "sourceType": "frame",
+ "purpose": "Static Figma status-bar battery glyph",
+ "bounds": {
+ "x": 336.67,
+ "y": 16.33,
+ "width": 24.33,
+ "height": 11.33
+ },
+ "exportFormat": "svg",
+ "bytes": 1055,
+ "sha256": "e4d18984f66b6e24e3ecf95ac7a893e3ac2e863a0b52c02ff075860b30a8b837",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "wifi.svg",
+ "sourceNode": "859:2038",
+ "equivalentNodes": ["859:2258", "859:2465"],
+ "sourceName": "Wifi",
+ "sourceType": "boolean-operation",
+ "purpose": "Static Figma status-bar Wi-Fi glyph",
+ "bounds": {
+ "x": 316.34,
+ "y": 16.33,
+ "width": 15.33,
+ "height": 11.01
+ },
+ "exportFormat": "svg",
+ "bytes": 1655,
+ "sha256": "d84c6cdf1a0f03a824a5b40174d8f326b62301a9c83e90a65600cfb04f1d9411",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "cellular.svg",
+ "sourceNode": "859:2042",
+ "equivalentNodes": ["859:2262", "859:2469"],
+ "sourceName": "Cellular Connection",
+ "sourceType": "boolean-operation",
+ "purpose": "Static Figma status-bar cellular glyph",
+ "bounds": { "x": 294.34, "y": 15, "width": 17, "height": 11.33 },
+ "exportFormat": "svg",
+ "bytes": 1183,
+ "sha256": "f5df0579b8435fef2e8a8cd8db3998c72947a409f44d25a5486f91f8adb745f3",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "mock-avatar.png",
+ "sourceNode": "859:2181",
+ "equivalentNodes": [
+ "859:2209",
+ "859:2401",
+ "859:2416",
+ "859:2610",
+ "859:2625"
+ ],
+ "sourceName": "image 4626",
+ "sourceType": "rounded-rectangle",
+ "purpose": "Figma reference avatar used only by the explicit mock visual-QA mode",
+ "bounds": { "width": 61.29, "height": 61.29 },
+ "exportFormat": "png",
+ "bytes": 186219,
+ "sha256": "31f699f244b15bdace7b0c655f254ecd79de89428bcaaae7d8d8738a65b2dcea",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": true,
+ "testOnly": true
+ }
+ ]
+}
diff --git a/activity/cp-space/fami/assets/layers/mock-avatar.png b/activity/cp-space/fami/assets/layers/mock-avatar.png
new file mode 100644
index 0000000..4e7409d
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/mock-avatar.png differ
diff --git a/activity/cp-space/fami/assets/layers/pill-brother.png b/activity/cp-space/fami/assets/layers/pill-brother.png
new file mode 100644
index 0000000..49cbcc1
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/pill-brother.png differ
diff --git a/activity/cp-space/fami/assets/layers/pill-cp-center.png b/activity/cp-space/fami/assets/layers/pill-cp-center.png
new file mode 100644
index 0000000..d2fefa0
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/pill-cp-center.png differ
diff --git a/activity/cp-space/fami/assets/layers/pill-cp-left.png b/activity/cp-space/fami/assets/layers/pill-cp-left.png
new file mode 100644
index 0000000..607459c
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/pill-cp-left.png differ
diff --git a/activity/cp-space/fami/assets/layers/pill-cp-right.png b/activity/cp-space/fami/assets/layers/pill-cp-right.png
new file mode 100644
index 0000000..f9a5182
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/pill-cp-right.png differ
diff --git a/activity/cp-space/fami/assets/layers/pill-sister.png b/activity/cp-space/fami/assets/layers/pill-sister.png
new file mode 100644
index 0000000..32609fc
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/pill-sister.png differ
diff --git a/activity/cp-space/fami/assets/layers/tab-brother.png b/activity/cp-space/fami/assets/layers/tab-brother.png
new file mode 100644
index 0000000..a2f5e12
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/tab-brother.png differ
diff --git a/activity/cp-space/fami/assets/layers/tab-cp.png b/activity/cp-space/fami/assets/layers/tab-cp.png
new file mode 100644
index 0000000..1054b19
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/tab-cp.png differ
diff --git a/activity/cp-space/fami/assets/layers/tab-sister.png b/activity/cp-space/fami/assets/layers/tab-sister.png
new file mode 100644
index 0000000..e7284e1
Binary files /dev/null and b/activity/cp-space/fami/assets/layers/tab-sister.png differ
diff --git a/activity/cp-space/fami/assets/layers/wifi.svg b/activity/cp-space/fami/assets/layers/wifi.svg
new file mode 100644
index 0000000..e5802d2
--- /dev/null
+++ b/activity/cp-space/fami/assets/layers/wifi.svg
@@ -0,0 +1,3 @@
+
diff --git a/activity/cp-space/fami/fami.css b/activity/cp-space/fami/fami.css
new file mode 100644
index 0000000..d47aa88
--- /dev/null
+++ b/activity/cp-space/fami/fami.css
@@ -0,0 +1,681 @@
+@font-face {
+ font-family: 'Source Han Sans SC Fami';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('./assets/fonts/source-han-sans-sc-medium-latin.woff2')
+ format('woff2');
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+html,
+body {
+ margin: 0;
+ width: 100%;
+ min-height: 100%;
+ overflow-x: hidden;
+ background: #440b23;
+ color: #fff;
+ font-family:
+ 'SF Pro Text', 'SF Pro Display', 'Noto Sans Arabic', Arial,
+ 'Helvetica Neue', Helvetica, sans-serif;
+}
+
+button {
+ border: 0;
+ padding: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ cursor: pointer;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.fami-page {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ min-height: 100svh;
+ background: #440b23;
+}
+
+.fami-page[data-tab='brother'] {
+ background: #080636;
+}
+
+.fami-page[data-tab='sister'] {
+ background: #301008;
+}
+
+.fami-stage {
+ --design-scale: 1;
+ --design-height: 812px;
+ --stage-height: 812px;
+ --accent: #ff0979;
+ --accent-soft: #ff5762;
+ --tab-bg: rgba(94, 13, 35, 0.8);
+ --tab-glow: #ff7948;
+ --tab-jewel-left: -7px;
+ --avatar-top: 35px;
+ --center-height: 120px;
+ --exp-top: 102px;
+ --progress-from: #ff1e37;
+ --progress-mid: #fe6a30;
+ --progress-to: #ffcf67;
+ position: relative;
+ flex: 0 0 auto;
+ width: min(100vw, 414px);
+ height: var(--stage-height);
+ overflow: hidden;
+ background: #440b23;
+}
+
+.fami-page[data-tab='brother'] .fami-stage {
+ --accent: #5709ff;
+ --accent-soft: #5709ff;
+ --tab-bg: rgba(17, 13, 94, 0.8);
+ --tab-glow: #4872ff;
+ --tab-jewel-left: 110px;
+ --avatar-top: 41px;
+ --center-height: 136px;
+ --exp-top: 102px;
+ --progress-from: #471eff;
+ --progress-mid: #30bcfe;
+ --progress-to: #67acff;
+ background: #080636;
+}
+
+.fami-page[data-tab='sister'] .fami-stage {
+ --accent: #ff4a09;
+ --accent-soft: #ff4a09;
+ --tab-bg: rgba(94, 29, 13, 0.8);
+ --tab-glow: #ff7948;
+ --tab-jewel-left: 220px;
+ --avatar-top: 41px;
+ --center-height: 136px;
+ --exp-top: 93px;
+ --progress-from: #ff5e1e;
+ --progress-mid: #fe6a30;
+ --progress-to: #ffb167;
+ background: #301008;
+}
+
+.fami-design {
+ position: relative;
+ width: 375px;
+ height: var(--design-height);
+ transform: scale(var(--design-scale));
+ transform-origin: left top;
+}
+
+.fami-background,
+.fami-card-skin,
+.fami-tab-jewel,
+.fami-center-art,
+.fami-avatar-frame,
+.fami-level-badge img,
+.fami-status-bar img,
+.fami-nav-button img {
+ display: block;
+ user-select: none;
+ -webkit-user-drag: none;
+ pointer-events: none;
+}
+
+.fami-background {
+ position: absolute;
+ z-index: 0;
+ inset: 0;
+ width: 375px;
+ height: 812px;
+ object-fit: fill;
+ object-position: center top;
+}
+
+.fami-status-bar {
+ position: absolute;
+ z-index: 5;
+ left: 0;
+ top: 0;
+ width: 375px;
+ height: 44px;
+}
+
+.fami-page.has-native-status .fami-status-bar {
+ visibility: hidden;
+}
+
+.fami-status-time {
+ position: absolute;
+ left: 33.87px;
+ top: 13px;
+ width: 54px;
+ height: 15px;
+ color: #fff;
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 15px;
+ font-weight: 400;
+ line-height: 15px;
+ text-align: center;
+}
+
+.fami-status-cellular {
+ position: absolute;
+ left: 294.34px;
+ top: 15px;
+ width: 17px;
+ height: 11.33px;
+}
+
+.fami-status-wifi {
+ position: absolute;
+ left: 316.34px;
+ top: 16.33px;
+ width: 15.33px;
+ height: 11.01px;
+}
+
+.fami-status-battery {
+ position: absolute;
+ left: 336.67px;
+ top: 16.33px;
+ width: 24.33px;
+ height: 11.33px;
+}
+
+.fami-nav {
+ position: absolute;
+ z-index: 5;
+ left: 0;
+ top: 44px;
+ width: 375px;
+ height: 44px;
+}
+
+.fami-nav h1 {
+ position: absolute;
+ left: 139px;
+ top: 13px;
+ width: 97px;
+ height: 18px;
+ margin: 0;
+ color: #fff;
+ font-family: 'Source Han Sans SC Fami', 'Noto Sans Arabic', sans-serif;
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 18px;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.fami-nav-button {
+ position: absolute;
+ top: 10px;
+ width: 24px;
+ height: 24px;
+}
+
+.fami-nav-button--back {
+ left: 16px;
+}
+
+.fami-nav-button--help {
+ left: 335px;
+}
+
+.fami-nav-button img {
+ width: 24px;
+ height: 24px;
+ object-fit: contain;
+}
+
+.fami-nav-button--help img {
+ width: 25.4px;
+ height: 25.4px;
+ transform: translate(-0.7px, -0.7px);
+}
+
+.fami-tabs {
+ position: absolute;
+ z-index: 5;
+ left: 16px;
+ top: 109px;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ width: 343px;
+ height: 37px;
+ border: 0.5px solid #f4b747;
+ border-radius: 32px;
+ background: var(--tab-bg);
+ box-shadow: inset 0 0 5.5px var(--tab-glow);
+ direction: ltr;
+}
+
+.fami-tab-jewel {
+ position: absolute;
+ z-index: 0;
+ left: calc(var(--tab-jewel-left) - 0.5px);
+ top: -2.5px;
+ width: 130px;
+ height: 48px;
+ object-fit: fill;
+}
+
+.fami-tabs button {
+ position: relative;
+ z-index: 1;
+ height: 37px;
+ color: rgba(255, 255, 255, 0.8);
+ font-family: 'SF Pro Text', Arial, sans-serif;
+ font-size: 18px;
+ font-weight: 400;
+ line-height: 18px;
+ text-align: center;
+}
+
+.fami-tabs button[aria-selected='true'] {
+ color: #fff;
+ font-family: 'Source Han Sans SC Fami', 'Noto Sans Arabic', sans-serif;
+ font-weight: 500;
+}
+
+.fami-tabs button[data-tab='brother'][aria-selected='false'],
+.fami-tabs button[data-tab='sister'][aria-selected='false'] {
+ letter-spacing: 0.5px;
+}
+
+.fami-tabs button[data-tab='brother'][aria-selected='false'] {
+ letter-spacing: 0.57px;
+}
+
+.fami-tabs button[data-tab='cp'][aria-selected='true'] {
+ letter-spacing: 0.35px;
+}
+
+.fami-tabs button[data-tab='brother'][aria-selected='true'] {
+ letter-spacing: -0.384px;
+}
+
+.fami-tabs button[data-tab='sister'][aria-selected='true'] {
+ letter-spacing: 0.042px;
+}
+
+.fami-tabs button span {
+ position: relative;
+ top: -1px;
+}
+
+.fami-tabs button[data-tab='cp'][aria-selected='true'] span {
+ left: -0.5px;
+}
+
+.fami-tabs button[data-tab='brother'] span,
+.fami-tabs button[data-tab='sister'] span {
+ left: 1px;
+}
+
+.fami-content {
+ position: absolute;
+ z-index: 3;
+ left: 0;
+ top: 0;
+ width: 375px;
+}
+
+.fami-relation-list {
+ position: absolute;
+ left: 14px;
+ top: 169px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ width: 347px;
+}
+
+.fami-relation-card {
+ position: relative;
+ flex: 0 0 199px;
+ width: 347px;
+ height: 199px;
+ color: var(--accent);
+}
+
+.fami-card-skin {
+ position: absolute;
+ z-index: 0;
+ inset: 0;
+ width: 347px;
+ height: 199px;
+ object-fit: fill;
+}
+
+.fami-avatar {
+ position: absolute;
+ z-index: 2;
+ top: var(--avatar-top);
+ width: 66px;
+ height: 66px;
+ margin: 0;
+}
+
+.fami-avatar--left {
+ left: 32px;
+}
+
+.fami-avatar--right {
+ left: 250px;
+}
+
+.fami-avatar-photo {
+ position: absolute;
+ z-index: 1;
+ left: 2.36px;
+ top: 2.36px;
+ display: grid;
+ place-items: center;
+ width: 61.29px;
+ height: 61.29px;
+ overflow: hidden;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.26);
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.fami-avatar-photo img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.fami-avatar-frame {
+ position: absolute;
+ z-index: 2;
+ inset: 0;
+ width: 66px;
+ height: 66px;
+ object-fit: contain;
+}
+
+.fami-center-art-wrap {
+ position: absolute;
+ z-index: 1;
+ left: 106px;
+ top: 6px;
+ width: 136px;
+ height: var(--center-height);
+ text-align: center;
+}
+
+.fami-center-art {
+ position: absolute;
+ z-index: 0;
+ inset: 0;
+ width: 136px;
+ height: var(--center-height);
+ object-fit: fill;
+}
+
+.fami-days-value {
+ position: absolute;
+ z-index: 1;
+ left: 50%;
+ top: 45px;
+ color: var(--accent);
+ font-size: 18px;
+ font-family: 'SF Pro Text', Arial, sans-serif;
+ font-weight: 400;
+ line-height: 18px;
+ text-shadow:
+ 0 1px 0 #fff,
+ 0 0 4px rgba(255, 255, 255, 0.75);
+ transform: translateX(-50%);
+}
+
+.fami-days-unit {
+ position: absolute;
+ z-index: 1;
+ left: 50%;
+ top: 70px;
+ color: var(--accent);
+ font-family: 'SF Pro Text', Arial, sans-serif;
+ font-size: 8px;
+ font-weight: 400;
+ line-height: 8px;
+ text-shadow: 0 1px 0 #fff;
+ transform: translateX(-50%);
+}
+
+.fami-exp-pill {
+ position: absolute;
+ z-index: 3;
+ left: 119px;
+ top: var(--exp-top);
+ display: grid;
+ place-items: center;
+ width: 112px;
+ height: 28px;
+}
+
+.fami-exp-pill-skin {
+ position: absolute;
+ z-index: 0;
+ inset: 0;
+ display: none;
+ width: 112px;
+ height: 28px;
+ pointer-events: none;
+}
+
+.fami-page[data-tab='cp'] .fami-exp-pill-skin--cp,
+.fami-page[data-tab='brother'] .fami-exp-pill-skin--brother,
+.fami-page[data-tab='sister'] .fami-exp-pill-skin--sister {
+ display: block;
+}
+
+.fami-exp-pill-skin--cp img {
+ position: absolute;
+ top: 0;
+ display: block;
+ height: 28px;
+ pointer-events: none;
+}
+
+.fami-exp-pill-skin--cp img:nth-child(1) {
+ left: 0;
+ width: 49px;
+}
+
+.fami-exp-pill-skin--cp img:nth-child(2) {
+ left: 49px;
+ width: 14px;
+}
+
+.fami-exp-pill-skin--cp img:nth-child(3) {
+ left: 63px;
+ width: 49px;
+}
+
+.fami-exp-pill [data-value='exp-away'] {
+ position: relative;
+ z-index: 1;
+ color: var(--accent);
+ font-family: 'SF Pro Text', Arial, sans-serif;
+ font-size: 8px;
+ font-weight: 400;
+ line-height: 8px;
+ text-align: center;
+ text-shadow:
+ 0 1px 0 rgba(255, 255, 255, 0.95),
+ 0 0 2px rgba(255, 255, 255, 0.85);
+ white-space: nowrap;
+}
+
+.fami-progress-block {
+ position: absolute;
+ z-index: 3;
+ left: 32px;
+ top: 134px;
+ width: 289px;
+ height: 35px;
+}
+
+.fami-level-badge {
+ position: absolute;
+ top: 1.5px;
+ width: 24px;
+ height: 13px;
+}
+
+.fami-level-badge--left {
+ left: 0;
+}
+
+.fami-level-badge--right {
+ right: 0;
+}
+
+.fami-level-badge img {
+ position: absolute;
+ inset: 0;
+ width: 24px;
+ height: 13px;
+ object-fit: fill;
+}
+
+.fami-level-badge span {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ color: #fff;
+ font-family: 'SF Pro Text', Arial, sans-serif;
+ font-size: 9px;
+ font-weight: 500;
+ line-height: 9px;
+}
+
+.fami-progress-track {
+ position: absolute;
+ left: 28px;
+ top: 0;
+ width: 233px;
+ height: 16px;
+ overflow: hidden;
+ border-radius: 74px;
+ background: rgba(255, 255, 255, 0.3);
+ backdrop-filter: blur(6px);
+}
+
+.fami-progress-fill {
+ position: absolute;
+ left: 1px;
+ top: 1px;
+ width: 1px;
+ height: 14px;
+ overflow: hidden;
+ border-radius: 7px;
+ background: linear-gradient(
+ 90deg,
+ var(--progress-from) 0%,
+ var(--progress-mid) 50%,
+ var(--progress-to) 100%
+ );
+ box-shadow:
+ inset -4px 0 3px #fffef8,
+ inset 0 2px 2px rgba(255, 254, 239, 0.48),
+ inset 0 -2px 2px rgba(255, 255, 255, 0.45);
+}
+
+.fami-progress-fill::after {
+ position: absolute;
+ inset: 1px 2px;
+ border-radius: inherit;
+ background: radial-gradient(
+ circle,
+ rgba(255, 255, 255, 0.95) 0 1px,
+ transparent 2px
+ )
+ 0 0 / 7px 7px;
+ content: '';
+ mix-blend-mode: screen;
+ opacity: 0.9;
+}
+
+.fami-progress-text {
+ position: absolute;
+ left: 50%;
+ top: 2px;
+ color: #fff;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 14px;
+ text-align: center;
+ white-space: nowrap;
+ transform: translateX(-50%);
+}
+
+.fami-time-text {
+ position: absolute;
+ left: 0;
+ top: 20px;
+ width: 289px;
+ margin: 0;
+ color: var(--accent-soft);
+ font-family: 'SF Pro Text', Arial, sans-serif;
+ font-size: 11px;
+ font-weight: 400;
+ line-height: 15px;
+ text-align: center;
+ text-shadow:
+ 0 1px 0 rgba(255, 255, 255, 0.82),
+ 0 0 2px rgba(255, 255, 255, 0.72);
+}
+
+.fami-state {
+ position: absolute;
+ left: 14px;
+ top: 169px;
+ display: grid;
+ place-items: center;
+ width: 347px;
+ min-height: 199px;
+ padding: 24px;
+ border: 1px solid rgba(255, 255, 255, 0.45);
+ border-radius: 12px;
+ background: rgba(20, 8, 35, 0.76);
+ color: #fff;
+ font-size: 14px;
+ line-height: 20px;
+ text-align: center;
+}
+
+.fami-state[data-retry='true'] {
+ cursor: pointer;
+}
+
+.fami-nav-button:focus-visible,
+.fami-tabs button:focus-visible,
+.fami-state:focus-visible {
+ outline: 2px solid var(--hy-theme-focus-ring, rgba(255, 255, 255, 0.75));
+ outline-offset: 2px;
+}
+
+[dir='rtl'] .fami-nav-button--back img {
+ transform: scaleX(-1);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ * {
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/activity/cp-space/fami/fami.js b/activity/cp-space/fami/fami.js
new file mode 100644
index 0000000..3010582
--- /dev/null
+++ b/activity/cp-space/fami/fami.js
@@ -0,0 +1,673 @@
+(function () {
+ 'use strict';
+
+ var VALID_TABS = ['cp', 'brother', 'sister'];
+ var LAYER_BASE = './fami/assets/layers/';
+ var THEMES = {
+ cp: {
+ background: 'bg-cp.png',
+ tab: 'tab-cp.png',
+ card: 'card-cp.png',
+ center: 'center-cp.png',
+ level: 'level-cp.png',
+ },
+ brother: {
+ background: 'bg-brother.png',
+ tab: 'tab-brother.png',
+ card: 'card-brother.png',
+ center: 'center-brother.png',
+ level: 'level-brother.png',
+ },
+ sister: {
+ background: 'bg-sister.png',
+ tab: 'tab-sister.png',
+ card: 'card-sister.png',
+ center: 'center-sister.png',
+ level: 'level-sister.png',
+ },
+ };
+
+ var page = document.getElementById('famiPage');
+ var stage = document.getElementById('famiStage');
+ var design = document.getElementById('famiDesign');
+ var background = document.getElementById('famiBackground');
+ var tabJewel = document.getElementById('famiTabJewel');
+ var relationList = document.getElementById('famiRelationList');
+ var stateButton = document.getElementById('famiStateButton');
+ var content = document.getElementById('famiContent');
+ var template = document.getElementById('famiRelationTemplate');
+ var backButton = document.getElementById('famiBackButton');
+ var helpButton = document.getElementById('famiHelpButton');
+ var tabButtons = Array.prototype.slice.call(
+ document.querySelectorAll('.fami-tabs [data-tab]')
+ );
+ var params = new URLSearchParams(window.location.search || '');
+ var resizeObserver = null;
+
+ var state = {
+ activeTab: normalizeTab(params.get('tab')) || 'cp',
+ mock: params.get('mock') === '1',
+ tabs: {
+ cp: createTabState(),
+ brother: createTabState(),
+ sister: createTabState(),
+ },
+ };
+
+ function createTabState() {
+ return {
+ loading: false,
+ loaded: false,
+ error: '',
+ relationships: [],
+ requestId: 0,
+ };
+ }
+
+ function normalizeTab(value) {
+ var tab = String(value || '')
+ .trim()
+ .toLowerCase();
+ return VALID_TABS.indexOf(tab) >= 0 ? tab : '';
+ }
+
+ function readNumber(value, fallback) {
+ var number = Number(value);
+ return Number.isFinite(number) ? number : fallback || 0;
+ }
+
+ function readString(value) {
+ return String(value == null ? '' : value).trim();
+ }
+
+ function firstValue() {
+ for (var index = 0; index < arguments.length; index += 1) {
+ var value = arguments[index];
+ if (value !== undefined && value !== null && value !== '') {
+ return value;
+ }
+ }
+ return undefined;
+ }
+
+ function hasValue(value) {
+ return value !== undefined && value !== null && value !== '';
+ }
+
+ function t(key, fallback) {
+ return window.HyAppI18n && window.HyAppI18n.t
+ ? window.HyAppI18n.t(key, fallback)
+ : fallback || key;
+ }
+
+ function formatNumber(value) {
+ var number = Math.max(0, Math.floor(readNumber(value, 0)));
+ return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ }
+
+ function layer(fileName) {
+ return LAYER_BASE + fileName;
+ }
+
+ function setSource(node, fileName) {
+ if (!node) return;
+ var source = layer(fileName);
+ if (node.getAttribute('src') !== source) {
+ node.setAttribute('src', source);
+ }
+ }
+
+ function profileFromData(raw) {
+ var profile = raw || {};
+ return {
+ id: readString(profile.user_id || profile.userId || profile.id),
+ displayId: readString(
+ profile.display_user_id || profile.displayUserId
+ ),
+ name: readString(
+ profile.username ||
+ profile.nickname ||
+ profile.display_name ||
+ profile.name
+ ),
+ avatar: readString(
+ profile.avatar ||
+ profile.avatar_url ||
+ profile.avatarUrl ||
+ profile.avatar_path
+ ),
+ };
+ }
+
+ function daysFromTimestamp(value) {
+ var formedAt = readNumber(value, 0);
+ if (formedAt <= 0) return 0;
+ if (formedAt < 100000000000) formedAt *= 1000;
+ if (Date.now() <= formedAt) return 1;
+ return Math.floor((Date.now() - formedAt) / 86400000) + 1;
+ }
+
+ function normalizeLevel(value) {
+ var level = Math.floor(readNumber(value, 1));
+ return Math.max(1, Math.min(99, level));
+ }
+
+ function normalizeRelationship(raw, tab) {
+ var item = raw || {};
+ var level = normalizeLevel(item.level || item.current_level);
+ var intimacy = Math.max(
+ 0,
+ readNumber(item.intimacy_value || item.intimacyValue, 0)
+ );
+ var currentThreshold = Math.max(
+ 0,
+ readNumber(
+ item.current_level_threshold || item.currentLevelThreshold,
+ 0
+ )
+ );
+ var nextThreshold = Math.max(
+ currentThreshold,
+ readNumber(item.next_level_threshold || item.nextLevelThreshold, 0)
+ );
+ var currentExp = hasValue(firstValue(item.current_exp, item.currentExp))
+ ? Math.max(
+ 0,
+ readNumber(firstValue(item.current_exp, item.currentExp))
+ )
+ : Math.max(0, intimacy - currentThreshold);
+ var nextExp = hasValue(firstValue(item.next_exp, item.nextExp))
+ ? Math.max(0, readNumber(firstValue(item.next_exp, item.nextExp)))
+ : Math.max(0, nextThreshold - currentThreshold);
+ if (!nextExp && intimacy > 0) nextExp = intimacy;
+ var neededExp = hasValue(
+ firstValue(item.needed_for_next_level, item.neededExp)
+ )
+ ? Math.max(
+ 0,
+ readNumber(
+ firstValue(item.needed_for_next_level, item.neededExp)
+ )
+ )
+ : Math.max(0, nextExp - currentExp);
+ var explicitProgress = firstValue(
+ item.level_progress_percent,
+ item.progress_percent,
+ item.progressPercent
+ );
+ var progressPercent = hasValue(explicitProgress)
+ ? readNumber(explicitProgress, 0)
+ : nextExp > 0
+ ? (currentExp / nextExp) * 100
+ : item.max_level
+ ? 100
+ : 0;
+ var days = Math.max(
+ 0,
+ readNumber(
+ firstValue(
+ item.days,
+ item.relationship_days,
+ daysFromTimestamp(item.formed_at_ms || item.formedAtMS)
+ ),
+ 0
+ )
+ );
+
+ return {
+ id: readString(
+ item.relationship_id || item.relationshipId || item.id
+ ),
+ relationType:
+ normalizeTab(item.relation_type || item.relationType) || tab,
+ status: readString(item.status || 'active').toLowerCase(),
+ level: level,
+ nextLevel: normalizeLevel(
+ firstValue(item.next_level, item.nextLevel, level + 1)
+ ),
+ days: days,
+ togetherDays: Math.max(
+ 0,
+ readNumber(
+ firstValue(
+ item.together_days,
+ item.togetherDays,
+ item.time_spent_days,
+ days
+ ),
+ days
+ )
+ ),
+ currentExp: currentExp,
+ nextExp: nextExp,
+ neededExp: neededExp,
+ progressPercent: Math.max(0, Math.min(100, progressPercent)),
+ maxLevel: Boolean(item.max_level || item.maxLevel),
+ me: profileFromData(item.me || item.me_profile),
+ partner: profileFromData(
+ item.partner || item.partner_profile || item.cp_user
+ ),
+ };
+ }
+
+ function activeRelationships(payload, tab) {
+ var data = payload || {};
+ var rows = Array.isArray(data)
+ ? data
+ : data.relationships || data.items || [];
+ return (Array.isArray(rows) ? rows : [])
+ .map(function (item) {
+ return normalizeRelationship(item, tab);
+ })
+ .filter(function (item) {
+ return (
+ item.relationType === tab &&
+ (!item.status || item.status === 'active')
+ );
+ });
+ }
+
+ function mockRelationship(tab) {
+ return normalizeRelationship(
+ {
+ relationship_id: 'figma-' + tab,
+ relation_type: tab,
+ status: 'active',
+ level: 1,
+ next_level: 2,
+ days: 999,
+ together_days: 4,
+ current_exp: 1000,
+ next_exp: 200000,
+ needed_for_next_level: 888,
+ level_progress_percent: 22.3175965665,
+ me: {
+ user_id: 'figma-left',
+ username: 'Fami',
+ avatar: layer('mock-avatar.png'),
+ },
+ partner: {
+ user_id: 'figma-right',
+ username: 'Fami',
+ avatar: layer('mock-avatar.png'),
+ },
+ },
+ tab
+ );
+ }
+
+ function renderAvatar(node, profile) {
+ if (!node) return;
+ node.replaceChildren();
+ var data = profile || {};
+ if (data.avatar) {
+ var image = document.createElement('img');
+ image.src = data.avatar;
+ image.alt = data.name || '';
+ image.addEventListener('error', function () {
+ image.remove();
+ node.textContent = (data.name || data.displayId || '?')
+ .charAt(0)
+ .toUpperCase();
+ });
+ node.appendChild(image);
+ return;
+ }
+ node.textContent = (data.name || data.displayId || '?')
+ .charAt(0)
+ .toUpperCase();
+ }
+
+ function renderRelationship(item, tab) {
+ var theme = THEMES[tab];
+ var fragment = template.content.cloneNode(true);
+ var card = fragment.querySelector('.fami-relation-card');
+ var levelText = t('cpSpace.levelValue', 'Lv.{level}').replace(
+ '{level}',
+ formatNumber(item.level)
+ );
+ var nextLevel = item.maxLevel ? item.level : item.nextLevel;
+ var expAwayText = item.maxLevel
+ ? t('cpSpace.maxLevelExp', '{exp} EXP').replace(
+ '{exp}',
+ formatNumber(item.currentExp)
+ )
+ : t('cpSpace.expAway', '{exp} Exp away from Lv. {level}')
+ .replace('{exp}', formatNumber(item.neededExp))
+ .replace('{level}', formatNumber(nextLevel));
+
+ setSource(card.querySelector('[data-layer="card"]'), theme.card);
+ setSource(card.querySelector('[data-layer="center"]'), theme.center);
+ card.querySelectorAll('[data-layer="level"]').forEach(function (node) {
+ setSource(node, theme.level);
+ });
+ renderAvatar(card.querySelector('[data-avatar="me"]'), item.me);
+ renderAvatar(
+ card.querySelector('[data-avatar="partner"]'),
+ item.partner
+ );
+ card.querySelector('[data-value="days"]').textContent = formatNumber(
+ item.days
+ );
+ card.querySelector('[data-value="exp-away"]').textContent = expAwayText;
+ // 两侧徽标在 Figma 中表示这段关系当前等级,不是“当前/下一等级”;下一等级只出现在经验提示里。
+ card.querySelector('[data-value="level-left"]').textContent = levelText;
+ card.querySelector('[data-value="level-right"]').textContent =
+ levelText;
+ card.querySelector('[data-value="progress-text"]').textContent =
+ String(Math.max(0, Math.floor(item.currentExp))) +
+ '/' +
+ String(Math.max(0, Math.floor(item.nextExp)));
+ card.querySelector('[data-value="progress"]').style.width =
+ Math.max(1, Math.min(233, (item.progressPercent / 100) * 233)) +
+ 'px';
+ card.querySelector('[data-value="time"]').textContent = t(
+ 'cpSpace.timeTogether',
+ 'Time spent together: {days} days'
+ ).replace('{days}', formatNumber(item.togetherDays));
+ card.setAttribute(
+ 'aria-label',
+ t('cpSpace.levelCard', 'Best friend level {level}').replace(
+ '{level}',
+ formatNumber(item.level)
+ )
+ );
+ return fragment;
+ }
+
+ function renderTheme() {
+ var tab = state.activeTab;
+ var theme = THEMES[tab];
+ page.setAttribute('data-tab', tab);
+ setSource(background, theme.background);
+ setSource(tabJewel, theme.tab);
+ tabButtons.forEach(function (button) {
+ var selected = button.getAttribute('data-tab') === tab;
+ button.setAttribute('aria-selected', String(selected));
+ button.tabIndex = selected ? 0 : -1;
+ if (selected) content.setAttribute('aria-labelledby', button.id);
+ });
+ }
+
+ function setStateMessage(message, retry) {
+ stateButton.hidden = !message;
+ stateButton.textContent = message || '';
+ stateButton.setAttribute('data-retry', String(Boolean(retry)));
+ stateButton.disabled = !retry;
+ }
+
+ function updateDesignHeight(cardCount) {
+ var count = Math.max(1, Number(cardCount || 0));
+ var designHeight = Math.max(
+ 812,
+ 169 + count * 199 + (count - 1) * 14 + 40
+ );
+ design.style.setProperty('--design-height', designHeight + 'px');
+ stage.style.setProperty('--design-height', designHeight + 'px');
+ syncScale();
+ }
+
+ function render() {
+ renderTheme();
+ var tabState = state.tabs[state.activeTab];
+ content.setAttribute('aria-busy', String(tabState.loading));
+ relationList.replaceChildren();
+
+ if (tabState.loading && !tabState.loaded) {
+ setStateMessage(t('cpSpace.loading', 'Loading...'), false);
+ updateDesignHeight(0);
+ return;
+ }
+ if (tabState.error) {
+ setStateMessage(
+ tabState.error ||
+ t('cpSpace.loadFailed', 'Load failed. Tap to retry.'),
+ true
+ );
+ updateDesignHeight(0);
+ return;
+ }
+ if (!tabState.relationships.length) {
+ setStateMessage(
+ t('cpSpace.empty', 'No relationship data yet.'),
+ false
+ );
+ updateDesignHeight(0);
+ return;
+ }
+
+ setStateMessage('', false);
+ tabState.relationships.forEach(function (item) {
+ relationList.appendChild(renderRelationship(item, state.activeTab));
+ });
+ updateDesignHeight(tabState.relationships.length);
+ }
+
+ function syncScale() {
+ if (!stage) return;
+ var width = stage.getBoundingClientRect().width || window.innerWidth;
+ var scale = Math.max(0.1, width / 375);
+ var designHeight = parseFloat(
+ stage.style.getPropertyValue('--design-height') || '812'
+ );
+ stage.style.setProperty('--design-scale', String(scale));
+ stage.style.setProperty('--stage-height', designHeight * scale + 'px');
+ }
+
+ function bindScale() {
+ syncScale();
+ if (window.ResizeObserver) {
+ resizeObserver = new ResizeObserver(syncScale);
+ resizeObserver.observe(stage);
+ return;
+ }
+ window.addEventListener('resize', syncScale);
+ window.addEventListener('orientationchange', syncScale);
+ }
+
+ function syncURL(tab) {
+ if (!window.history || !window.history.replaceState) return;
+ var url = new URL(window.location.href);
+ url.searchParams.set('tab', tab);
+ // tab 只属于本页视觉和查询状态;replaceState 避免每次切换都污染 App WebView 的返回栈。
+ window.history.replaceState(null, '', url.toString());
+ }
+
+ function loadTab(tab, force) {
+ var target = normalizeTab(tab) || 'cp';
+ var tabState = state.tabs[target];
+ if (!force && (tabState.loading || tabState.loaded)) {
+ render();
+ return Promise.resolve(tabState.relationships);
+ }
+ var requestId = ++tabState.requestId;
+ tabState.loading = true;
+ tabState.error = '';
+ render();
+
+ if (state.mock) {
+ tabState.relationships = [mockRelationship(target)];
+ tabState.loaded = true;
+ tabState.loading = false;
+ render();
+ return Promise.resolve(tabState.relationships);
+ }
+
+ if (
+ !window.HyAppAPI ||
+ !window.HyAppAPI.cpSpace ||
+ !window.HyAppAPI.cpSpace.relationships
+ ) {
+ tabState.loading = false;
+ tabState.error = t(
+ 'cpSpace.loadFailed',
+ 'Load failed. Tap to retry.'
+ );
+ render();
+ return Promise.reject(new Error('cp_space_api_unavailable'));
+ }
+
+ // 关系类型和分页值原样交给 gateway;页面不猜测用户关系,也不把一个 tab 的结果复制到另一个 tab。
+ return window.HyAppAPI.cpSpace
+ .relationships({
+ relation_type: target,
+ page: 1,
+ page_size: target === 'cp' ? 1 : 20,
+ })
+ .then(function (payload) {
+ if (requestId !== tabState.requestId) return [];
+ tabState.relationships = activeRelationships(payload, target);
+ tabState.loaded = true;
+ return tabState.relationships;
+ })
+ .catch(function (error) {
+ if (requestId !== tabState.requestId) return [];
+ tabState.error =
+ (error && error.message) ||
+ t('cpSpace.loadFailed', 'Load failed. Tap to retry.');
+ return [];
+ })
+ .then(function (rows) {
+ if (requestId !== tabState.requestId) return rows;
+ tabState.loading = false;
+ render();
+ return rows;
+ });
+ }
+
+ function setActiveTab(tab, options) {
+ var target = normalizeTab(tab) || 'cp';
+ state.activeTab = target;
+ render();
+ if (!options || options.syncURL !== false) syncURL(target);
+ return loadTab(target, Boolean(options && options.force));
+ }
+
+ function hasNativeBridge() {
+ return Boolean(
+ (window.flutter_inappwebview &&
+ window.flutter_inappwebview.callHandler) ||
+ (window.HyAppBridgeChannel &&
+ window.HyAppBridgeChannel.postMessage) ||
+ (window.FlutterBridge && window.FlutterBridge.postMessage) ||
+ (window.webkit &&
+ window.webkit.messageHandlers &&
+ window.webkit.messageHandlers.HyAppBridge)
+ );
+ }
+
+ function contextualURL(path) {
+ var url = new URL(path, window.location.href);
+ ['token', 'access_token', 'env', 'language', 'lang', 'locale'].forEach(
+ function (key) {
+ if (params.has(key)) url.searchParams.set(key, params.get(key));
+ }
+ );
+ url.searchParams.set('app_code', 'fami');
+ return url.toString();
+ }
+
+ function applyExternalData(payload) {
+ var data = payload || {};
+ VALID_TABS.forEach(function (tab) {
+ if (!data[tab]) return;
+ state.tabs[tab].relationships = activeRelationships(
+ Array.isArray(data[tab])
+ ? { relationships: data[tab] }
+ : data[tab],
+ tab
+ );
+ state.tabs[tab].loaded = true;
+ state.tabs[tab].loading = false;
+ state.tabs[tab].error = '';
+ });
+ if (normalizeTab(data.activeTab)) state.activeTab = data.activeTab;
+ render();
+ }
+
+ function bindEvents() {
+ tabButtons.forEach(function (button) {
+ button.addEventListener('click', function () {
+ setActiveTab(button.getAttribute('data-tab'));
+ });
+ button.addEventListener('keydown', function (event) {
+ var current = VALID_TABS.indexOf(
+ button.getAttribute('data-tab')
+ );
+ var next = current;
+ if (event.key === 'ArrowLeft') next = current - 1;
+ else if (event.key === 'ArrowRight') next = current + 1;
+ else if (event.key === 'Home') next = 0;
+ else if (event.key === 'End') next = VALID_TABS.length - 1;
+ else return;
+ event.preventDefault();
+ // 标签视觉顺序固定为从左到右;键盘同样按可见顺序循环,RTL 只翻转返回箭头而不倒置关系类型。
+ next = (next + VALID_TABS.length) % VALID_TABS.length;
+ var targetButton = tabButtons[next];
+ targetButton.focus();
+ setActiveTab(targetButton.getAttribute('data-tab'));
+ });
+ });
+
+ stateButton.addEventListener('click', function () {
+ if (stateButton.getAttribute('data-retry') === 'true') {
+ loadTab(state.activeTab, true);
+ }
+ });
+
+ backButton.addEventListener('click', function () {
+ if (
+ window.FlutterPageControl &&
+ window.FlutterPageControl.postMessage
+ ) {
+ window.FlutterPageControl.postMessage('close_page');
+ return;
+ }
+ if (hasNativeBridge() && window.HyAppBridge) {
+ window.HyAppBridge.back();
+ return;
+ }
+ window.history.back();
+ });
+
+ helpButton.addEventListener('click', function () {
+ if (hasNativeBridge() && window.HyAppBridge) {
+ window.HyAppBridge.post('cpSpaceHelpTap', {
+ page: 'fami-cp-space',
+ relation_type: state.activeTab,
+ });
+ return;
+ }
+ window.location.href = contextualURL('../../cp-rules/');
+ });
+
+ window.addEventListener('hyapp:i18n-ready', render);
+ window.addEventListener('hyapp:cp-space-fami-data', function (event) {
+ applyExternalData(event.detail || {});
+ });
+ window.addEventListener('hyapp:cp-space-data', function (event) {
+ applyExternalData(event.detail || {});
+ });
+ }
+
+ if (hasNativeBridge() && params.get('figma_status') !== '1') {
+ page.classList.add('has-native-status');
+ }
+ bindScale();
+ bindEvents();
+ if (window.HyAppCPSpaceFamiData) {
+ applyExternalData(window.HyAppCPSpaceFamiData);
+ setActiveTab(state.activeTab, { syncURL: false });
+ } else {
+ setActiveTab(state.activeTab, { syncURL: false });
+ }
+
+ if (window.HyAppBridge && window.HyAppBridge.ready) {
+ window.HyAppBridge.ready({
+ page: 'fami-cp-space',
+ app_code: 'fami',
+ tabs: VALID_TABS.slice(),
+ mock: state.mock,
+ });
+ }
+})();
diff --git a/activity/cp-space/fami/product.js b/activity/cp-space/fami/product.js
new file mode 100644
index 0000000..1e07d42
--- /dev/null
+++ b/activity/cp-space/fami/product.js
@@ -0,0 +1,8 @@
+(function () {
+ 'use strict';
+
+ // common/api.js 在未显式传 app_code 时读取页面默认值;必须在 API 脚本执行前声明,避免 Fami 页面误落到 Lalu 租户。
+ window.HyAppDefaultAppCode = 'fami';
+ window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
+ document.documentElement.setAttribute('data-hy-app-code', 'fami');
+})();
diff --git a/activity/cp/huwaa.html b/activity/cp/huwaa.html
new file mode 100644
index 0000000..90c8a37
--- /dev/null
+++ b/activity/cp/huwaa.html
@@ -0,0 +1,205 @@
+
+
+
+
+
+ Last Weekly CP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Last Weekly CP
+
+
+
+
+
+

+
00
+
D
+
+
+

+
00
+
H
+
+
+

+
00
+
M
+
+
+

+
00
+
S
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/cp/huwaa/assets/layers/bg-texture.webp b/activity/cp/huwaa/assets/layers/bg-texture.webp
new file mode 100644
index 0000000..1140070
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/bg-texture.webp differ
diff --git a/activity/cp/huwaa/assets/layers/countdown-heart.webp b/activity/cp/huwaa/assets/layers/countdown-heart.webp
new file mode 100644
index 0000000..040dc0a
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/countdown-heart.webp differ
diff --git a/activity/cp/huwaa/assets/layers/crown-bronze.webp b/activity/cp/huwaa/assets/layers/crown-bronze.webp
new file mode 100644
index 0000000..12bb65b
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/crown-bronze.webp differ
diff --git a/activity/cp/huwaa/assets/layers/crown-gold.webp b/activity/cp/huwaa/assets/layers/crown-gold.webp
new file mode 100644
index 0000000..8cd6b8f
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/crown-gold.webp differ
diff --git a/activity/cp/huwaa/assets/layers/crown-silver.webp b/activity/cp/huwaa/assets/layers/crown-silver.webp
new file mode 100644
index 0000000..494d6d0
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/crown-silver.webp differ
diff --git a/activity/cp/huwaa/assets/layers/deco-line-left.webp b/activity/cp/huwaa/assets/layers/deco-line-left.webp
new file mode 100644
index 0000000..46c513b
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/deco-line-left.webp differ
diff --git a/activity/cp/huwaa/assets/layers/deco-line-right.webp b/activity/cp/huwaa/assets/layers/deco-line-right.webp
new file mode 100644
index 0000000..9d403c0
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/deco-line-right.webp differ
diff --git a/activity/cp/huwaa/assets/layers/gift-plane.webp b/activity/cp/huwaa/assets/layers/gift-plane.webp
new file mode 100644
index 0000000..f9fa674
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/gift-plane.webp differ
diff --git a/activity/cp/huwaa/assets/layers/hero-art-text-free.webp b/activity/cp/huwaa/assets/layers/hero-art-text-free.webp
new file mode 100644
index 0000000..7099bda
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/hero-art-text-free.webp differ
diff --git a/activity/cp/huwaa/assets/layers/manifest.json b/activity/cp/huwaa/assets/layers/manifest.json
new file mode 100644
index 0000000..b24916d
--- /dev/null
+++ b/activity/cp/huwaa/assets/layers/manifest.json
@@ -0,0 +1,424 @@
+{
+ "schemaVersion": 1,
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "canvas": {
+ "id": "543:1998",
+ "name": "Huwaa运营"
+ },
+ "entryFrames": {
+ "plaza": "831:3320",
+ "rank": "831:4048",
+ "rewards": "831:3913"
+ },
+ "designSizes": {
+ "plaza": {
+ "width": 1080,
+ "height": 4283
+ },
+ "rank": {
+ "width": 1080,
+ "height": 3733
+ },
+ "rewards": {
+ "width": 1080,
+ "height": 4782
+ }
+ },
+ "forbiddenAggregateNodes": [
+ "543:1998",
+ "823:3005",
+ "831:3320",
+ "831:4048",
+ "831:3913",
+ "831:3323",
+ "831:3326",
+ "831:3339",
+ "831:3349",
+ "831:3642",
+ "831:4381",
+ "831:3942"
+ ]
+ },
+ "assets": [
+ {
+ "file": "bg-texture.webp",
+ "sourceNode": "831:3321",
+ "sourceName": "花瓣素材_情人节白色情人节七夕520梦幻浪漫感粉丝爱心i底纹背景元素素材_193406578 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Pure petal texture positioned at x=-487, y=489 over the independent #e98c9b stage fill with Figma opacity 0.5 and screen blend mode",
+ "bounds": {
+ "width": 2415,
+ "height": 4293
+ },
+ "original": {
+ "width": 1242,
+ "height": 2208
+ },
+ "exportFormat": "webp",
+ "bytes": 711912,
+ "sha256": "e0c9a81e2bb893731f8c70cf25f322693da52fe12e8a48513d4d7de8bd6d182c",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "derivedFromSha256": "9b13664bd6b77c64075da4891a95acef6116cc2055f3c9d9fda794493a8b6ede",
+ "transform": "Figma raw image fill encoded as WebP quality 94; runtime CSS preserves the source node bounds, opacity and screen blend over the frame fill"
+ },
+ {
+ "file": "hero-art-text-free.webp",
+ "sourceNode": "831:3325",
+ "sourceName": "c7ffd4a5-20dc-41c9-93f6-7721e460aa70 2",
+ "sourceType": "RECTANGLE",
+ "purpose": "Text-free event illustration behind the independent localized DOM wordmark; countdown, tabs, state and business data remain separate DOM layers",
+ "bounds": {
+ "width": 1080,
+ "height": 1333
+ },
+ "original": {
+ "width": 1080,
+ "height": 1333
+ },
+ "exportFormat": "webp",
+ "bytes": 356714,
+ "sha256": "33620a230108ca0559062de12263356349c26b0b18ffbb863e5966ea23d6e194",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "derivedFromSha256": "ad52de31140cb9d72415f86b570c44aff121b0000e1aef88470b28472c1c4137",
+ "transform": "Localized inpainting was limited to the embedded wordmark region; before WebP encoding, normalized source pixels above y=830 and below y=1275 were preserved, while the title was moved to independent localized DOM text",
+ "preEncodingPixelExactCoverage": 0.6981
+ },
+ {
+ "file": "countdown-heart.webp",
+ "sourceNode": "831:3327",
+ "sourceName": "ffutuft 3",
+ "sourceType": "RECTANGLE",
+ "purpose": "Empty countdown heart skin behind live DOM values",
+ "bounds": {
+ "width": 163,
+ "height": 106
+ },
+ "original": {
+ "width": 163,
+ "height": 106
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 20946,
+ "sha256": "d0a83f2b9695c2d15330d25203947dad171c9f03afbfb9d053e444765f0220fb",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "tab-active.webp",
+ "sourceNode": "831:3347",
+ "sourceName": "图层 0 34",
+ "sourceType": "RECTANGLE",
+ "purpose": "Active tab skin without its DOM label or interaction target",
+ "bounds": {
+ "width": 303,
+ "height": 126
+ },
+ "original": {
+ "width": 303,
+ "height": 126
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 40482,
+ "sha256": "dcf5231b062cfc09529493285ee42e35b7f3b0e1c1295b5fd8432573cdf9a811",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "tab-inactive.webp",
+ "sourceNode": "831:3344",
+ "sourceName": "图层 0 34",
+ "sourceType": "RECTANGLE",
+ "purpose": "Inactive tab skin without its DOM label or interaction target",
+ "bounds": {
+ "width": 303,
+ "height": 126
+ },
+ "original": {
+ "width": 303,
+ "height": 126
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 37228,
+ "sha256": "6aa5d9166eac1894fd64f014004a58afbb0a0930aa3967562cb82fb829315e91",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "panel-skin.webp",
+ "sourceNode": "831:3350",
+ "sourceName": "想吃vxcvxvdsfsdf 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Reusable empty pink-and-gold panel skin rendered with CSS nine-slicing",
+ "bounds": {
+ "width": 960,
+ "height": 671
+ },
+ "original": {
+ "width": 1254,
+ "height": 1254
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 865498,
+ "sha256": "b3011ae288d78d5f92a5e8d610eb889088798ab9061b739b403ad95e2ce1e5b4",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "gift-plane.webp",
+ "sourceNode": "831:3568",
+ "sourceName": "06黄金小飞机缩略图 (1) 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Decorative gift icon between CP partners in the plaza list",
+ "bounds": {
+ "width": 152,
+ "height": 152
+ },
+ "original": {
+ "width": 152,
+ "height": 152
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 40268,
+ "sha256": "880f57ca597bd1625700aaf3869fba1daeaefdf921c0807ff6a242695bc2e714",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "deco-line-right.webp",
+ "sourceNode": "831:3583",
+ "sourceName": "520 (431) 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Right-side heartbeat line between the CP avatar and center icon",
+ "bounds": {
+ "width": 159,
+ "height": 30
+ },
+ "original": {
+ "width": 159,
+ "height": 30
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 1600,
+ "sha256": "f2ecdcfa3a07d3fe9ac5b19de4d9de9c3aa802093a62f8e8227ce6a79333b23d",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "deco-line-left.webp",
+ "sourceNode": "831:3584",
+ "sourceName": "520 (431) 2",
+ "sourceType": "RECTANGLE",
+ "purpose": "Left-side heartbeat line between the CP avatar and center icon",
+ "bounds": {
+ "width": 159,
+ "height": 30
+ },
+ "original": {
+ "width": 159,
+ "height": 30
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 1612,
+ "sha256": "5e8afa2c2ab1b6117961bc9ae1162ff50213eb4be2b034293eb7421c69b246b6",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "score-heart.webp",
+ "sourceNode": "831:3849",
+ "sourceName": "ffutuft 2",
+ "sourceType": "RECTANGLE",
+ "purpose": "Empty score heart behind live DOM rank values",
+ "bounds": {
+ "width": 220,
+ "height": 143
+ },
+ "original": {
+ "width": 220,
+ "height": 143
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 35024,
+ "sha256": "4e9052c29e375553b9fecf77dbba8bfaab51b94fba30c00b500b31c776a1496b",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "podium-gold.webp",
+ "sourceNode": "831:4421",
+ "sourceName": "图层 0 31",
+ "sourceType": "RECTANGLE",
+ "purpose": "First-place double-avatar podium frame without avatars or names",
+ "bounds": {
+ "width": 716,
+ "height": 434
+ },
+ "original": {
+ "width": 716,
+ "height": 434
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 278480,
+ "sha256": "01cab5779b58c65256b25d2dd7784a73d777c5b9a2f75d4dc22df0ea062e3a5d",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "podium-silver.webp",
+ "sourceNode": "831:4426",
+ "sourceName": "04144 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Second-place double-avatar podium frame without avatars or names",
+ "bounds": {
+ "width": 500,
+ "height": 284
+ },
+ "original": {
+ "width": 500,
+ "height": 284
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 125178,
+ "sha256": "13dd006d1840d1fd0ed5545446bd2a493293a3a64e8b333b9b88e3e95cbb3e57",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "podium-bronze.webp",
+ "sourceNode": "831:4437",
+ "sourceName": "轻微我去恶 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Third-place double-avatar podium frame without avatars or names",
+ "bounds": {
+ "width": 500,
+ "height": 286
+ },
+ "original": {
+ "width": 500,
+ "height": 286
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 137328,
+ "sha256": "060b52606840c41717664c9dc5b5721d3f69c10305d79b2f935d1f20353e65a8",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "reward-frame.webp",
+ "sourceNode": "831:3950",
+ "sourceName": "8d9ca1c38fb3b780ff6b5d6d3cd318b8f108e7b83f3cd-59aVD7 4",
+ "sourceType": "RECTANGLE",
+ "purpose": "Empty reward wreath placed around live reward cover images",
+ "bounds": {
+ "width": 200,
+ "height": 200
+ },
+ "original": {
+ "width": 200,
+ "height": 200
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 39558,
+ "sha256": "e26e9f171b44296471cd9fd0c84fed4f0835c30f54e7d48435a18162aa62bfe7",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "crown-gold.webp",
+ "sourceNode": "831:3946",
+ "sourceName": "冠军 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Gold reward-tier crown",
+ "bounds": {
+ "width": 162,
+ "height": 144
+ },
+ "original": {
+ "width": 162,
+ "height": 144
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 24612,
+ "sha256": "b9c26c70a5a2f22273639bcd68c4af63cd87bc104cc93676b5f6feaaf7eacaaa",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "crown-silver.webp",
+ "sourceNode": "831:3981",
+ "sourceName": "冠军 2",
+ "sourceType": "RECTANGLE",
+ "purpose": "Silver reward-tier crown",
+ "bounds": {
+ "width": 162,
+ "height": 144
+ },
+ "original": {
+ "width": 162,
+ "height": 144
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 23164,
+ "sha256": "21a95c439ca10aa615ab1b03db6f3c23f68fd8b22ddbd1690d723baf27be53f3",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "crown-bronze.webp",
+ "sourceNode": "831:4016",
+ "sourceName": "冠军 2",
+ "sourceType": "RECTANGLE",
+ "purpose": "Bronze reward-tier crown",
+ "bounds": {
+ "width": 162,
+ "height": 144
+ },
+ "original": {
+ "width": 162,
+ "height": 144
+ },
+ "exportFormat": "webp-lossless",
+ "bytes": 25124,
+ "sha256": "5dbba6acd0eb8f92ac9278b9720a3aba8fcaef97716a26d8db47b68ee4201dca",
+ "containsText": false,
+ "containsDynamicText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ }
+ ]
+}
diff --git a/activity/cp/huwaa/assets/layers/panel-skin.webp b/activity/cp/huwaa/assets/layers/panel-skin.webp
new file mode 100644
index 0000000..8c073c4
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/panel-skin.webp differ
diff --git a/activity/cp/huwaa/assets/layers/podium-bronze.webp b/activity/cp/huwaa/assets/layers/podium-bronze.webp
new file mode 100644
index 0000000..0b8c8e7
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/podium-bronze.webp differ
diff --git a/activity/cp/huwaa/assets/layers/podium-gold.webp b/activity/cp/huwaa/assets/layers/podium-gold.webp
new file mode 100644
index 0000000..0a12bc5
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/podium-gold.webp differ
diff --git a/activity/cp/huwaa/assets/layers/podium-silver.webp b/activity/cp/huwaa/assets/layers/podium-silver.webp
new file mode 100644
index 0000000..a4c30b5
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/podium-silver.webp differ
diff --git a/activity/cp/huwaa/assets/layers/reward-frame.webp b/activity/cp/huwaa/assets/layers/reward-frame.webp
new file mode 100644
index 0000000..8940286
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/reward-frame.webp differ
diff --git a/activity/cp/huwaa/assets/layers/score-heart.webp b/activity/cp/huwaa/assets/layers/score-heart.webp
new file mode 100644
index 0000000..9c6c82e
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/score-heart.webp differ
diff --git a/activity/cp/huwaa/assets/layers/tab-active.webp b/activity/cp/huwaa/assets/layers/tab-active.webp
new file mode 100644
index 0000000..270ca56
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/tab-active.webp differ
diff --git a/activity/cp/huwaa/assets/layers/tab-inactive.webp b/activity/cp/huwaa/assets/layers/tab-inactive.webp
new file mode 100644
index 0000000..0bc66bc
Binary files /dev/null and b/activity/cp/huwaa/assets/layers/tab-inactive.webp differ
diff --git a/activity/cp/huwaa/huwaa.css b/activity/cp/huwaa/huwaa.css
new file mode 100644
index 0000000..b9880f9
--- /dev/null
+++ b/activity/cp/huwaa/huwaa.css
@@ -0,0 +1,832 @@
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+:root {
+ --huwaa-cp-page-width: 375px;
+ --huwaa-cp-design-scale: 0.3472222222;
+ --huwaa-cp-brand: var(--hy-theme-primary-strong, #c52d45);
+ --huwaa-cp-brand-deep: #a1152f;
+ --huwaa-cp-text: #a11626;
+ --huwaa-cp-muted: #dd708d;
+ --huwaa-cp-row: rgba(197, 45, 69, 0.13);
+ --huwaa-cp-page-bg: var(--hy-theme-bg, #f8c7d5);
+}
+
+html,
+body {
+ width: 100%;
+ min-height: 100%;
+ margin: 0;
+ overflow-x: hidden;
+ scrollbar-width: none;
+ background: var(--huwaa-cp-page-bg);
+ color: var(--hy-theme-text, #21182b);
+ font-family:
+ Inter, Arial, 'Helvetica Neue', Helvetica, 'Noto Sans', sans-serif;
+}
+
+html::-webkit-scrollbar,
+body::-webkit-scrollbar {
+ display: none;
+}
+
+button {
+ padding: 0;
+ border: 0;
+ outline: none;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ cursor: pointer;
+}
+
+img {
+ display: block;
+}
+
+.huwaa-cp-sr-only {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
+.huwaa-cp-viewport,
+.huwaa-cp-page,
+.huwaa-cp-stage-wrap {
+ position: relative;
+ width: var(--huwaa-cp-page-width);
+ max-width: 768px;
+ min-height: 1297px;
+ margin: 0 auto;
+ overflow: hidden;
+ background: var(--huwaa-cp-page-bg);
+}
+
+.huwaa-cp-stage-wrap {
+ min-height: 0;
+}
+
+.huwaa-cp-stage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ min-height: 3733px;
+ overflow: hidden;
+ direction: ltr;
+ transform: scale(var(--huwaa-cp-design-scale));
+ transform-origin: top left;
+ background: #e98c9b;
+}
+
+.huwaa-cp-stage > * {
+ position: absolute;
+}
+
+.huwaa-cp-background {
+ top: 489px;
+ left: -487px;
+ z-index: 0;
+ width: 2415px;
+ height: 4293px;
+ opacity: 0.5;
+ mix-blend-mode: screen;
+ object-fit: cover;
+ pointer-events: none;
+}
+
+.huwaa-cp-hero {
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 1080px;
+ height: 1333px;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.huwaa-cp-hero > img {
+ width: 1080px;
+ height: 1333px;
+ object-fit: cover;
+ -webkit-mask-image: linear-gradient(
+ to bottom,
+ #000 0,
+ #000 88.93%,
+ transparent 100%
+ );
+ mask-image: linear-gradient(
+ to bottom,
+ #000 0,
+ #000 88.93%,
+ transparent 100%
+ );
+}
+
+.huwaa-cp-wordmark {
+ position: absolute;
+ top: 856px;
+ left: 20px;
+ z-index: 2;
+ display: grid;
+ width: 1040px;
+ min-height: 370px;
+ margin: 0;
+ place-items: center;
+ color: #ffe0b2;
+ font-family: Georgia, 'Times New Roman', serif;
+ font-size: 181px;
+ font-style: italic;
+ font-weight: 800;
+ letter-spacing: -8px;
+ line-height: 0.82;
+ overflow-wrap: normal;
+ paint-order: stroke fill;
+ text-align: center;
+ text-shadow:
+ 0 8px 0 #7f1d17,
+ 0 13px 22px rgba(91, 10, 25, 0.66),
+ 0 0 12px rgba(255, 248, 212, 0.78);
+ text-wrap: balance;
+ -webkit-text-stroke: 7px #b75f31;
+ unicode-bidi: plaintext;
+}
+
+.huwaa-cp-countdown {
+ top: 1333px;
+ left: 52px;
+ z-index: 3;
+ width: 974px;
+ height: 106px;
+}
+
+.huwaa-cp-time-unit {
+ position: absolute;
+ top: 0;
+ width: 223px;
+ height: 106px;
+}
+
+.huwaa-cp-time-unit:nth-child(1) {
+ left: 0;
+}
+
+.huwaa-cp-time-unit:nth-child(2) {
+ left: 250px;
+}
+
+.huwaa-cp-time-unit:nth-child(3) {
+ left: 501px;
+}
+
+.huwaa-cp-time-unit:nth-child(4) {
+ left: 756px;
+}
+
+.huwaa-cp-time-unit img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 163px;
+ height: 106px;
+}
+
+.huwaa-cp-time-unit strong {
+ position: absolute;
+ top: 25px;
+ left: 0;
+ z-index: 1;
+ width: 163px;
+ color: #fff;
+ font-size: 40px;
+ font-weight: 900;
+ line-height: 40px;
+ text-align: center;
+ text-shadow:
+ 0 2px 0 rgba(176, 47, 78, 0.52),
+ 0 0 12px rgba(255, 255, 255, 0.78);
+}
+
+.huwaa-cp-time-unit span {
+ position: absolute;
+ top: 25px;
+ left: 194px;
+ width: 31px;
+ color: #fff;
+ font-size: 40px;
+ font-weight: 900;
+ line-height: 40px;
+ text-align: center;
+ text-shadow: 0 2px 0 rgba(176, 47, 78, 0.52);
+}
+
+.huwaa-cp-tabs {
+ top: 1495px;
+ left: 39px;
+ z-index: 5;
+ display: grid;
+ width: 1000px;
+ height: 126px;
+ grid-template-columns: repeat(3, 303px);
+ column-gap: 46px;
+}
+
+.huwaa-cp-tab {
+ position: relative;
+ width: 303px;
+ height: 126px;
+ color: rgba(161, 22, 52, 0.34);
+ font-size: 40px;
+ font-weight: 800;
+ line-height: 40px;
+ text-align: center;
+ text-shadow: 0 2px 1px rgba(255, 255, 255, 0.78);
+}
+
+.huwaa-cp-tab img {
+ position: absolute;
+ inset: 0;
+ width: 303px;
+ height: 126px;
+ pointer-events: none;
+}
+
+.huwaa-cp-tab span {
+ position: absolute;
+ top: 34px;
+ left: 20px;
+ width: 263px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ unicode-bidi: plaintext;
+}
+
+html[lang='es'] .huwaa-cp-tab[data-tab='rewards'] {
+ font-size: 32px;
+}
+
+.huwaa-cp-tab::after {
+ position: absolute;
+ inset: -12px 0;
+ content: '';
+}
+
+.huwaa-cp-tab.is-active {
+ color: #ff4766;
+ text-shadow:
+ 0 2px 0 rgba(255, 255, 255, 0.9),
+ 0 0 10px rgba(255, 139, 166, 0.36);
+}
+
+.huwaa-cp-tab:focus-visible {
+ border-radius: 58px;
+ box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.78);
+}
+
+.huwaa-cp-tab-content {
+ top: 0;
+ left: 0;
+ z-index: 4;
+ width: 1080px;
+}
+
+.huwaa-cp-copyright {
+ left: 114px;
+ z-index: 4;
+ width: 850px;
+ height: 30px;
+ color: #fff;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 30px;
+ text-align: center;
+ text-shadow: 0 2px 8px rgba(163, 48, 76, 0.3);
+}
+
+.huwaa-cp-panel {
+ position: absolute;
+ left: 60px;
+ width: 960px;
+ border: 84px solid transparent;
+ border-image-source: url('./assets/layers/panel-skin.webp');
+ border-image-slice: 110 fill;
+ border-image-width: 84px;
+ border-image-repeat: stretch;
+ color: var(--huwaa-cp-text);
+}
+
+.huwaa-cp-panel-body {
+ position: absolute;
+ inset: -84px;
+}
+
+.huwaa-cp-panel-title,
+.huwaa-cp-panel-subtitle {
+ position: absolute;
+ left: 55px;
+ width: 850px;
+ margin: 0;
+ text-align: center;
+ unicode-bidi: plaintext;
+}
+
+.huwaa-cp-panel-title {
+ top: 75px;
+ min-height: 45px;
+ color: #b82e2d;
+ font-size: 32px;
+ font-weight: 800;
+ line-height: 36px;
+}
+
+.huwaa-cp-panel-subtitle {
+ top: 140px;
+ min-height: 27px;
+ color: var(--huwaa-cp-muted);
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 30px;
+}
+
+.huwaa-cp-panel.is-gift-panel .huwaa-cp-panel-title {
+ top: 75px;
+ line-height: 36px;
+}
+
+.huwaa-cp-panel.is-gift-panel .huwaa-cp-panel-subtitle {
+ top: 166px;
+}
+
+.huwaa-cp-list {
+ position: absolute;
+ top: 203px;
+ left: 60px;
+ display: grid;
+ width: 840px;
+ gap: 24px;
+}
+
+.huwaa-cp-panel.is-gift-panel .huwaa-cp-list {
+ top: 229px;
+}
+
+.huwaa-cp-row {
+ position: relative;
+ width: 840px;
+ height: 199px;
+ overflow: hidden;
+ border-radius: 30px;
+ background: var(--huwaa-cp-row);
+}
+
+.huwaa-cp-avatar {
+ position: absolute;
+ z-index: 2;
+ display: grid;
+ width: 113px;
+ height: 113px;
+ place-items: center;
+ overflow: hidden;
+ border: 5px solid rgba(255, 235, 207, 0.96);
+ border-radius: 50%;
+ background: #f2b9c6;
+ box-shadow:
+ 0 0 0 3px rgba(183, 81, 64, 0.48),
+ 0 6px 14px rgba(143, 25, 52, 0.18);
+ color: #fff;
+ font-size: 34px;
+ font-weight: 800;
+}
+
+.huwaa-cp-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.huwaa-cp-row .huwaa-cp-avatar.is-left {
+ top: 26px;
+ left: 58px;
+}
+
+.huwaa-cp-row .huwaa-cp-avatar.is-right {
+ top: 26px;
+ left: 669px;
+}
+
+.huwaa-cp-name {
+ position: absolute;
+ top: 147px;
+ z-index: 3;
+ width: 144px;
+ overflow: hidden;
+ color: var(--huwaa-cp-text);
+ font-size: 26px;
+ font-weight: 700;
+ line-height: 30px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ unicode-bidi: plaintext;
+}
+
+.huwaa-cp-name.is-left {
+ left: 42px;
+}
+
+.huwaa-cp-name.is-right {
+ left: 653px;
+}
+
+.huwaa-cp-deco-line {
+ position: absolute;
+ top: 71px;
+ z-index: 1;
+ width: 159px;
+ height: 30px;
+}
+
+.huwaa-cp-deco-line.is-left {
+ left: 336px;
+}
+
+.huwaa-cp-deco-line.is-right {
+ left: 504px;
+}
+
+.huwaa-cp-gift {
+ position: absolute;
+ top: 15px;
+ left: 344px;
+ z-index: 2;
+ width: 152px;
+ height: 152px;
+ object-fit: contain;
+}
+
+.huwaa-cp-score-heart {
+ position: absolute;
+ top: 28px;
+ left: 308px;
+ z-index: 2;
+ width: 220px;
+ height: 143px;
+}
+
+.huwaa-cp-score {
+ position: absolute;
+ top: 71px;
+ left: 359px;
+ z-index: 3;
+ width: 120px;
+ overflow: hidden;
+ color: #ff4460;
+ font-size: 30px;
+ font-weight: 800;
+ line-height: 36px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.huwaa-cp-state {
+ position: absolute;
+ top: 50%;
+ left: 110px;
+ width: 740px;
+ transform: translateY(-50%);
+ color: var(--huwaa-cp-muted);
+ font-size: 30px;
+ font-weight: 700;
+ line-height: 42px;
+ text-align: center;
+ unicode-bidi: plaintext;
+}
+
+.huwaa-cp-state.is-loading::before {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-right: 14px;
+ border: 4px solid rgba(197, 45, 69, 0.2);
+ border-top-color: var(--huwaa-cp-brand);
+ border-radius: 50%;
+ content: '';
+ animation: huwaa-cp-spin 0.8s linear infinite;
+ vertical-align: -2px;
+}
+
+.huwaa-cp-retry {
+ position: relative;
+ display: block;
+ min-width: 200px;
+ height: 64px;
+ margin: 22px auto 0;
+ padding: 0 28px;
+ border-radius: 32px;
+ background: var(--hy-theme-button, #a868dc);
+ color: #fff;
+ font-size: 26px;
+ font-weight: 800;
+}
+
+.huwaa-cp-retry::after {
+ position: absolute;
+ inset: -43px -20px;
+ content: '';
+}
+
+.huwaa-cp-retry:focus-visible {
+ box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.82);
+}
+
+.huwaa-cp-rank-podium {
+ position: absolute;
+ top: 1677px;
+ left: 25px;
+ width: 1030px;
+ height: 751px;
+}
+
+.huwaa-cp-podium-art,
+.huwaa-cp-podium-avatar,
+.huwaa-cp-podium-name {
+ position: absolute;
+}
+
+.huwaa-cp-podium-art {
+ z-index: 2;
+ pointer-events: none;
+}
+
+.huwaa-cp-podium-avatar {
+ z-index: 1;
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ border-radius: 50%;
+ background: #efb2c1;
+ color: #fff;
+ font-weight: 900;
+ text-align: center;
+}
+
+.huwaa-cp-podium-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.huwaa-cp-podium-name {
+ z-index: 3;
+ overflow: hidden;
+ color: #fff;
+ font-weight: 700;
+ text-align: center;
+ text-overflow: ellipsis;
+ text-shadow: 0 2px 4px rgba(144, 33, 59, 0.36);
+ white-space: nowrap;
+ unicode-bidi: plaintext;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-art {
+ top: 0;
+ left: 157px;
+ width: 716px;
+ height: 434px;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-avatar {
+ top: 119px;
+ width: 223px;
+ height: 223px;
+ font-size: 54px;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-avatar.is-left {
+ left: 236px;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-avatar.is-right {
+ left: 557px;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-name {
+ top: 386px;
+ width: 315px;
+ height: 50px;
+ background: rgba(188, 30, 65, 0.26);
+ font-size: 35px;
+ line-height: 49px;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-name.is-left {
+ left: 219px;
+}
+
+.huwaa-cp-podium.is-gold .huwaa-cp-podium-name.is-right {
+ left: 497px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-art,
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-art {
+ top: 467px;
+ width: 500px;
+ height: 284px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-art {
+ left: 0;
+}
+
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-art {
+ top: 465px;
+ left: 530px;
+ height: 286px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-avatar,
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-avatar {
+ top: 550px;
+ width: 153px;
+ height: 153px;
+ font-size: 40px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-avatar.is-left {
+ left: 62px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-avatar.is-right {
+ left: 281px;
+}
+
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-avatar.is-left {
+ left: 598px;
+}
+
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-avatar.is-right {
+ left: 812px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-name,
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-name {
+ top: 716px;
+ width: 172px;
+ height: 34px;
+ background: rgba(188, 30, 65, 0.22);
+ font-size: 24px;
+ line-height: 34px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-name.is-left {
+ left: 94px;
+}
+
+.huwaa-cp-podium.is-silver .huwaa-cp-podium-name.is-right {
+ left: 281px;
+}
+
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-name.is-left {
+ left: 624px;
+}
+
+.huwaa-cp-podium.is-bronze .huwaa-cp-podium-name.is-right {
+ left: 811px;
+}
+
+.huwaa-cp-rank-empty {
+ position: absolute;
+ top: 1990px;
+ left: 170px;
+ z-index: 4;
+ width: 740px;
+ color: #fff;
+ font-size: 30px;
+ font-weight: 800;
+ line-height: 42px;
+ text-align: center;
+ text-shadow: 0 2px 8px rgba(161, 21, 47, 0.42);
+}
+
+.huwaa-cp-reward-panel .huwaa-cp-panel-body {
+ overflow: hidden;
+}
+
+.huwaa-cp-reward-crown {
+ position: absolute;
+ top: 75px;
+ left: 399px;
+ width: 162px;
+ height: 144px;
+ object-fit: contain;
+}
+
+.huwaa-cp-reward-grid {
+ position: absolute;
+ top: 260px;
+ left: 81px;
+ display: grid;
+ width: 800px;
+ height: 604px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ grid-template-columns: repeat(3, 200px);
+ grid-auto-rows: 272px;
+ column-gap: 100px;
+ row-gap: 60px;
+ scrollbar-width: none;
+}
+
+.huwaa-cp-reward-grid::-webkit-scrollbar {
+ display: none;
+}
+
+.huwaa-cp-reward-card {
+ position: relative;
+ width: 200px;
+ height: 272px;
+ color: var(--huwaa-cp-text);
+ text-align: center;
+}
+
+.huwaa-cp-reward-icon {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ overflow: hidden;
+ border-radius: 26px;
+ background: rgba(197, 45, 69, 0.11);
+}
+
+.huwaa-cp-reward-frame,
+.huwaa-cp-reward-cover {
+ position: absolute;
+ object-fit: contain;
+}
+
+.huwaa-cp-reward-frame {
+ inset: 0;
+ z-index: 2;
+ width: 200px;
+ height: 200px;
+}
+
+.huwaa-cp-reward-cover {
+ top: 48px;
+ left: 48px;
+ z-index: 1;
+ width: 104px;
+ height: 104px;
+ border-radius: 18px;
+}
+
+.huwaa-cp-reward-name {
+ display: -webkit-box;
+ max-height: 60px;
+ min-height: 36px;
+ margin-top: 19px;
+ overflow-wrap: anywhere;
+ font-size: 24px;
+ font-weight: 700;
+ line-height: 30px;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ unicode-bidi: plaintext;
+}
+
+.huwaa-cp-reward-note {
+ display: -webkit-box;
+ max-height: 44px;
+ margin-top: -2px;
+ color: var(--huwaa-cp-muted);
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 22px;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ unicode-bidi: plaintext;
+}
+
+@keyframes huwaa-cp-spin {
+ to {
+ transform: rotate(1turn);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .huwaa-cp-state.is-loading::before {
+ animation: none;
+ }
+}
diff --git a/activity/cp/huwaa/huwaa.js b/activity/cp/huwaa/huwaa.js
new file mode 100644
index 0000000..2df86dd
--- /dev/null
+++ b/activity/cp/huwaa/huwaa.js
@@ -0,0 +1,964 @@
+(function () {
+ 'use strict';
+
+ var DESIGN_WIDTH = 1080;
+ var MAX_PAGE_WIDTH = 768;
+ var ASSET_BASE = './huwaa/assets/layers/';
+ var PODIUM_ASSETS = {
+ gold: 'podium-gold.webp',
+ silver: 'podium-silver.webp',
+ bronze: 'podium-bronze.webp',
+ };
+ var CROWN_ASSETS = {
+ 1: 'crown-gold.webp',
+ 2: 'crown-silver.webp',
+ 3: 'crown-bronze.webp',
+ };
+ var STAGE_HEIGHTS = {
+ plaza: 4283,
+ rank: 3733,
+ rewards: 4782,
+ };
+ var PANEL_LAYOUT = {
+ plazaFirstTop: 1677,
+ plazaFirstHeight: 1168,
+ plazaSecondTop: 2904,
+ plazaSecondHeight: 1142,
+ rankPanelTop: 2484,
+ rankPanelHeight: 1142,
+ rewardFirstTop: 1677,
+ rewardPanelHeight: 960,
+ rewardPanelGap: 56,
+ };
+
+ var viewport = document.getElementById('huwaaCpViewport');
+ var page = document.getElementById('huwaaCpPage');
+ var stage = document.getElementById('huwaaCpStage');
+ var stageWrap = document.getElementById('huwaaCpStageWrap');
+ var tabContent = document.getElementById('huwaaCpTabContent');
+ var copyright = document.querySelector('.huwaa-cp-copyright');
+ var announcement = document.getElementById('huwaaCpAnnouncement');
+ var tabs = Array.prototype.slice.call(
+ document.querySelectorAll('.huwaa-cp-tab')
+ );
+
+ var requestGeneration = 0;
+ var countdownEndAt = 0;
+ var countdownTimer = 0;
+ var serverTimeOffsetMS = 0;
+ var activeTab = resolveInitialTab();
+ var activityData = {
+ statusState: 'loading',
+ plazaState: 'loading',
+ rank: [],
+ previousRows: [],
+ plazaRows: [],
+ rewards: [],
+ enabled: true,
+ };
+
+ function t(key, fallback) {
+ if (window.HyAppI18n && window.HyAppI18n.t) {
+ return window.HyAppI18n.t(key, fallback);
+ }
+ return fallback || key;
+ }
+
+ function escapeHTML(value) {
+ return String(value == null ? '' : value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ function safeImageURL(value) {
+ var source = String(value || '').trim();
+ if (!source) return '';
+ if (/^(https?:|data:image\/|blob:|\.\.?\/)/i.test(source)) {
+ return escapeHTML(source);
+ }
+ return '';
+ }
+
+ function resolveInitialTab() {
+ try {
+ var requested = new URLSearchParams(window.location.search).get(
+ 'tab'
+ );
+ if (['plaza', 'rank', 'rewards'].indexOf(requested) >= 0) {
+ return requested;
+ }
+ } catch (_) {
+ // 无 URLSearchParams 的旧 WebView 使用设计稿默认的 CP Rank,不影响核心接口加载。
+ }
+ return 'rank';
+ }
+
+ function isLocalRuntime() {
+ try {
+ var params = new URLSearchParams(window.location.search);
+ var hostname = String(window.location.hostname || '').toLowerCase();
+ return (
+ hostname === 'localhost' ||
+ hostname === '127.0.0.1' ||
+ hostname === '::1' ||
+ params.get('env') === 'local'
+ );
+ } catch (_) {
+ return false;
+ }
+ }
+
+ function isExplicitMockMode() {
+ try {
+ return (
+ isLocalRuntime() &&
+ new URLSearchParams(window.location.search).get('mock') === '1'
+ );
+ } catch (_) {
+ return false;
+ }
+ }
+
+ function twoDigits(value) {
+ return String(Math.max(0, Number(value) || 0)).padStart(2, '0');
+ }
+
+ function resolveExplicitEndAt() {
+ if (!isLocalRuntime()) return 0;
+ try {
+ var params = new URLSearchParams(window.location.search);
+ var parsed = Date.parse(
+ params.get('end_at') || params.get('endAt') || ''
+ );
+ return Number.isFinite(parsed) && parsed > Date.now() ? parsed : 0;
+ } catch (_) {
+ return 0;
+ }
+ }
+
+ function nowMS() {
+ return Date.now() + serverTimeOffsetMS;
+ }
+
+ function getTimeParts() {
+ var diff =
+ countdownEndAt > 0 ? Math.max(0, countdownEndAt - nowMS()) : 0;
+ var totalSeconds = Math.floor(diff / 1000);
+ return {
+ days: Math.floor(totalSeconds / 86400),
+ hours: Math.floor((totalSeconds % 86400) / 3600),
+ minutes: Math.floor((totalSeconds % 3600) / 60),
+ seconds: totalSeconds % 60,
+ };
+ }
+
+ function updateCountdown() {
+ var parts = getTimeParts();
+ Object.keys(parts).forEach(function (key) {
+ document
+ .querySelectorAll('[data-count="' + key + '"]')
+ .forEach(function (node) {
+ node.textContent = twoDigits(parts[key]);
+ });
+ });
+ }
+
+ function initials(name) {
+ var normalized = String(name || '').trim();
+ return normalized ? normalized.charAt(0).toUpperCase() : 'CP';
+ }
+
+ function renderAvatar(className, avatar, name) {
+ var imageURL = safeImageURL(avatar);
+ return [
+ '',
+ imageURL
+ ? '
'
+ : '' + escapeHTML(initials(name)) + '',
+ '',
+ ].join('');
+ }
+
+ function renderPodiumAvatar(className, avatar, name) {
+ var imageURL = safeImageURL(avatar);
+ return [
+ '',
+ imageURL
+ ? '
'
+ : '' + escapeHTML(initials(name)) + '',
+ '',
+ ].join('');
+ }
+
+ function renderState(state, retryScope) {
+ var key =
+ state === 'error'
+ ? 'cp.error'
+ : state === 'loading'
+ ? 'cp.loading'
+ : state === 'disabled'
+ ? 'cp.disabled'
+ : state === 'unavailable'
+ ? 'cp.unavailable'
+ : 'cp.empty';
+ var fallback =
+ state === 'error'
+ ? 'Unable to load activity data.'
+ : state === 'loading'
+ ? 'Loading...'
+ : state === 'disabled'
+ ? 'This activity is not available.'
+ : state === 'unavailable'
+ ? 'No data source is available yet.'
+ : 'No data yet.';
+ return [
+ '',
+ '',
+ escapeHTML(t(key, fallback)),
+ '',
+ retryScope
+ ? ''
+ : '',
+ '
',
+ ].join('');
+ }
+
+ function renderPanel(options) {
+ var classes = ['huwaa-cp-panel'];
+ if (options.className) classes.push(options.className);
+ return [
+ '',
+ '',
+ options.body,
+ '
',
+ '',
+ ].join('');
+ }
+
+ function renderPanelHeader(
+ titleKey,
+ titleFallback,
+ subtitleKey,
+ subtitleFallback
+ ) {
+ return [
+ '',
+ escapeHTML(t(titleKey, titleFallback)),
+ '
',
+ subtitleKey
+ ? '' +
+ escapeHTML(t(subtitleKey, subtitleFallback)) +
+ '
'
+ : '',
+ ].join('');
+ }
+
+ function renderCPRow(row) {
+ var center =
+ row.type === 'gift'
+ ? '
'
+ : [
+ '
',
+ '',
+ escapeHTML(row.score),
+ '',
+ ].join('');
+ return [
+ '',
+ renderAvatar('is-left', row.leftAvatar, row.leftName),
+ renderAvatar('is-right', row.rightAvatar, row.rightName),
+ '',
+ escapeHTML(row.leftName),
+ '',
+ '',
+ escapeHTML(row.rightName),
+ '',
+ '
',
+ '
',
+ center,
+ '',
+ ].join('');
+ }
+
+ function rowsOrState(rows, state, retryScope) {
+ if (rows.length) {
+ return (
+ '' +
+ rows.map(renderCPRow).join('') +
+ '
'
+ );
+ }
+ return renderState(
+ state === 'ready' ? 'empty' : state,
+ state === 'error' ? retryScope : ''
+ );
+ }
+
+ function renderPreviousPanel(top, height) {
+ var body =
+ renderPanelHeader(
+ 'cp.previousWeekTopTitle',
+ 'Previous Top 3',
+ 'cp.previousWeekTopSubtitle',
+ '(Top CPs from the previous completed week.)'
+ ) +
+ rowsOrState(
+ activityData.previousRows,
+ activityData.statusState,
+ 'status'
+ );
+ return renderPanel({ top: top, height: height, body: body });
+ }
+
+ function renderPlaza() {
+ var firstBody =
+ renderPanelHeader(
+ 'cp.huwaaPlazaGiftTitle',
+ 'Gifts exchanged between CPs ≥ 5000 gold will be displayed here',
+ 'cp.plazaGiftSubtitle',
+ '(Display the last 10 records, sorted in ascending order.)'
+ ) +
+ rowsOrState(activityData.plazaRows, activityData.plazaState, '');
+ return [
+ renderPanel({
+ top: PANEL_LAYOUT.plazaFirstTop,
+ height: PANEL_LAYOUT.plazaFirstHeight,
+ className: 'is-gift-panel',
+ body: firstBody,
+ }),
+ renderPreviousPanel(
+ PANEL_LAYOUT.plazaSecondTop,
+ PANEL_LAYOUT.plazaSecondHeight
+ ),
+ ].join('');
+ }
+
+ function renderPodiumPair(item, place) {
+ if (!item) return '';
+ var level = place === 1 ? 'gold' : place === 2 ? 'silver' : 'bronze';
+ var accessibleLabel = [
+ t('cp.rankPlace', 'Rank'),
+ String(place),
+ item.leftName,
+ item.rightName,
+ ].join(' ');
+ return [
+ '',
+ renderPodiumAvatar('is-left', item.leftAvatar, item.leftName),
+ renderPodiumAvatar('is-right', item.rightAvatar, item.rightName),
+ '
',
+ '',
+ escapeHTML(item.leftName),
+ '',
+ '',
+ escapeHTML(item.rightName),
+ '',
+ '',
+ ].join('');
+ }
+
+ function renderRank() {
+ var podium = '';
+ if (activityData.rank.length) {
+ var rankAt = function (place) {
+ return activityData.rank.find(function (item) {
+ return item.place === place;
+ });
+ };
+ podium = [
+ '',
+ renderPodiumPair(rankAt(1), 1),
+ renderPodiumPair(rankAt(2), 2),
+ renderPodiumPair(rankAt(3), 3),
+ '',
+ ].join('');
+ } else {
+ var rankStateKey =
+ activityData.statusState === 'error'
+ ? 'cp.error'
+ : activityData.statusState === 'loading'
+ ? 'cp.loading'
+ : activityData.statusState === 'disabled'
+ ? 'cp.disabled'
+ : 'cp.empty';
+ var rankStateFallback =
+ activityData.statusState === 'error'
+ ? 'Unable to load activity data.'
+ : activityData.statusState === 'loading'
+ ? 'Loading...'
+ : activityData.statusState === 'disabled'
+ ? 'This activity is not available.'
+ : 'No data yet.';
+ podium = [
+ '',
+ escapeHTML(t(rankStateKey, rankStateFallback)),
+ '
',
+ ].join('');
+ }
+ return (
+ podium +
+ renderPreviousPanel(
+ PANEL_LAYOUT.rankPanelTop,
+ PANEL_LAYOUT.rankPanelHeight
+ )
+ );
+ }
+
+ function rewardName(item) {
+ var resourceName =
+ item.resourceName ||
+ item.name ||
+ item.resource_name ||
+ localizedResourceType(item.resource_type || item.resourceType) ||
+ t('cp.reward', 'Reward');
+ var duration = Number(item.duration_days || item.durationDays || 0);
+ var quantity = Number(item.quantity || 0);
+ if (duration > 0) {
+ return (
+ resourceName + '*' + duration + ' ' + t('cp.daysLong', 'days')
+ );
+ }
+ if (quantity > 0) return resourceName + '*' + quantity;
+ return resourceName;
+ }
+
+ function localizedResourceType(value) {
+ var type = String(value || '')
+ .trim()
+ .toLowerCase();
+ var keys = {
+ avatar_frame: ['cp.rewardAvatarFrame', 'Avatar frame'],
+ profile_card: ['cp.rewardProfileCard', 'Profile card'],
+ coin: ['cp.rewardCoins', 'Coins'],
+ vehicle: ['cp.rewardVehicle', 'Vehicle'],
+ chat_bubble: ['cp.rewardChatBubble', 'Chat bubble'],
+ badge: ['cp.rewardBadge', 'Badge'],
+ floating_screen: ['cp.rewardFloatingScreen', 'Floating screen'],
+ gift: ['cp.rewardGift', 'Gift'],
+ mic_seat_icon: ['cp.rewardMicSeatIcon', 'Mic seat icon'],
+ mic_seat_animation: [
+ 'cp.rewardMicSeatAnimation',
+ 'Mic seat animation',
+ ],
+ emoji_pack: ['cp.rewardEmojiPack', 'Emoji pack'],
+ vip_trial_card: ['cp.rewardVipTrialCard', 'VIP trial card'],
+ };
+ return keys[type] ? t(keys[type][0], keys[type][1]) : '';
+ }
+
+ function rewardNote(item) {
+ return String(item.description || item.note || '').trim();
+ }
+
+ function renderRewardItem(item) {
+ var cover = safeImageURL(item.coverUrl || item.cover_url);
+ return [
+ '',
+ '',
+ cover
+ ? '

'
+ : '',
+ '

',
+ '
',
+ '',
+ escapeHTML(item.displayName || rewardName(item)),
+ '
',
+ item.note || rewardNote(item)
+ ? '' +
+ escapeHTML(item.note || rewardNote(item)) +
+ '
'
+ : '',
+ '',
+ ].join('');
+ }
+
+ function renderRewardPanel(section, index) {
+ var tierNo = Number(section.rankNo || index + 1);
+ if (tierNo < 1 || tierNo > 3) tierNo = index + 1;
+ var top =
+ PANEL_LAYOUT.rewardFirstTop +
+ index *
+ (PANEL_LAYOUT.rewardPanelHeight + PANEL_LAYOUT.rewardPanelGap);
+ var items = Array.isArray(section.items) ? section.items : [];
+ var tierLabels = {
+ 1: t('cp.rewardTierGold', 'First-place rewards'),
+ 2: t('cp.rewardTierSilver', 'Second-place rewards'),
+ 3: t('cp.rewardTierBronze', 'Third-place rewards'),
+ };
+ var body = [
+ '
',
+ items.length
+ ? '' +
+ items.map(renderRewardItem).join('') +
+ '
'
+ : renderState('empty', ''),
+ ].join('');
+ return renderPanel({
+ top: top,
+ height: PANEL_LAYOUT.rewardPanelHeight,
+ className: 'huwaa-cp-reward-panel',
+ ariaLabel: tierLabels[tierNo],
+ body: body,
+ });
+ }
+
+ function renderRewards() {
+ if (!activityData.rewards.length) {
+ var state =
+ activityData.statusState === 'ready'
+ ? 'empty'
+ : activityData.statusState;
+ return renderPanel({
+ top: PANEL_LAYOUT.rewardFirstTop,
+ height: PANEL_LAYOUT.rewardPanelHeight,
+ className: 'huwaa-cp-reward-panel',
+ body: renderState(state, state === 'error' ? 'status' : ''),
+ });
+ }
+ return [1, 2, 3]
+ .map(function (rankNo) {
+ return (
+ activityData.rewards.find(function (section) {
+ return section.rankNo === rankNo;
+ }) || { rankNo: rankNo, items: [] }
+ );
+ })
+ .map(renderRewardPanel)
+ .join('');
+ }
+
+ function formatScore(value) {
+ var number = Number(value || 0);
+ if (!Number.isFinite(number)) return '0';
+ try {
+ return number.toLocaleString(document.documentElement.lang || 'en');
+ } catch (_) {
+ return String(number);
+ }
+ }
+
+ function normalizePairUser(user) {
+ var source = user || {};
+ var userID = source.user_id || source.userId || '';
+ var displayID = source.display_user_id || source.displayUserId || '';
+ var name = source.username || source.name || displayID || userID || '';
+ if (!name && !source.avatar) return null;
+ return {
+ name: name || 'CP',
+ avatar: source.avatar || source.avatar_url || '',
+ };
+ }
+
+ function normalizeRankEntry(entry, fallbackPlace) {
+ var source = entry || {};
+ var userA = normalizePairUser(source.user_a || source.userA);
+ var userB = normalizePairUser(source.user_b || source.userB);
+ if (!userA || !userB) return null;
+ return {
+ type: 'score',
+ place: Number(source.rank || fallbackPlace || 0),
+ score: formatScore(source.score || source.intimacy_value || 0),
+ leftName: userA.name,
+ rightName: userB.name,
+ leftAvatar: userA.avatar,
+ rightAvatar: userB.avatar,
+ };
+ }
+
+ function normalizeRewardSection(section, fallbackRankNo) {
+ var source = section || {};
+ var group = source.resource_group || source.resourceGroup || {};
+ var groupItems = Array.isArray(group.items) ? group.items : [];
+ var items = Array.isArray(source.items) ? source.items : [];
+ return {
+ rankNo: Number(
+ source.rank_no || source.rankNo || fallbackRankNo || 0
+ ),
+ items: items.map(function (item, index) {
+ var groupItem = groupItems[index] || {};
+ var resource = groupItem.resource || {};
+ return {
+ resourceName: String(resource.name || '').trim(),
+ resource_type:
+ item.resource_type ||
+ item.resourceType ||
+ resource.resource_type ||
+ resource.resourceType ||
+ '',
+ duration_days: item.duration_days || item.durationDays || 0,
+ quantity: item.quantity || groupItem.quantity || 0,
+ coverUrl:
+ item.cover_url ||
+ item.coverUrl ||
+ resource.preview_url ||
+ resource.previewUrl ||
+ resource.asset_url ||
+ resource.assetUrl ||
+ '',
+ };
+ }),
+ };
+ }
+
+ function applyActivityStatus(status) {
+ var source = status || {};
+ var serverTimeMS = Number(
+ source.server_time_ms || source.serverTimeMS || 0
+ );
+ if (serverTimeMS > 0) {
+ serverTimeOffsetMS = serverTimeMS - Date.now();
+ }
+ activityData.enabled = source.enabled !== false;
+ if (!activityData.enabled) {
+ countdownEndAt = 0;
+ activityData.rank = [];
+ activityData.previousRows = [];
+ activityData.rewards = [];
+ activityData.statusState = 'disabled';
+ return;
+ }
+ var endMS = Number(
+ (source.period && (source.period.end_ms || source.period.endMS)) ||
+ source.end_ms ||
+ source.endMS ||
+ 0
+ );
+ countdownEndAt = endMS > nowMS() ? endMS : 0;
+
+ activityData.rank = Array.isArray(source.entries)
+ ? source.entries
+ .map(function (entry, index) {
+ return normalizeRankEntry(entry, index + 1);
+ })
+ .filter(Boolean)
+ .sort(function (left, right) {
+ return left.place - right.place;
+ })
+ .slice(0, 3)
+ : [];
+ activityData.previousRows = Array.isArray(source.previous_entries)
+ ? source.previous_entries
+ .map(function (entry, index) {
+ return normalizeRankEntry(entry, index + 1);
+ })
+ .filter(Boolean)
+ .slice(0, 3)
+ : [];
+ activityData.rewards = Array.isArray(source.rewards)
+ ? source.rewards
+ .map(function (section, index) {
+ return normalizeRewardSection(section, index + 1);
+ })
+ .filter(function (section) {
+ return section.rankNo >= 1 && section.rankNo <= 3;
+ })
+ .sort(function (left, right) {
+ return left.rankNo - right.rankNo;
+ })
+ : [];
+ activityData.statusState = 'ready';
+ }
+
+ function applyMockData() {
+ var pairs = [
+ ['Avery', 'Noor', 126580],
+ ['Mina', 'Elif', 118420],
+ ['Lina', 'Sofia', 109730],
+ ['Dara', 'Maya', 98760],
+ ];
+ function pair(row, index, type) {
+ return {
+ type: type || 'score',
+ place: index + 1,
+ score: formatScore(row[2]),
+ rawScore: row[2],
+ leftName: row[0],
+ rightName: row[1],
+ leftAvatar: '',
+ rightAvatar: '',
+ };
+ }
+ activityData.rank = pairs.slice(0, 3).map(function (row, index) {
+ return pair(row, index, 'score');
+ });
+ activityData.previousRows = pairs
+ .slice(0, 3)
+ .map(function (row, index) {
+ return pair(row, index, 'score');
+ });
+ activityData.plazaRows = pairs.map(function (row, index) {
+ return pair(row, index, 'gift');
+ });
+ activityData.rewards = ['gold', 'silver', 'bronze'].map(
+ function (_, tier) {
+ return {
+ rankNo: tier + 1,
+ items: [
+ {
+ displayName: t('cp.rewardMedal7', 'Medal*7 days'),
+ note:
+ tier === 0
+ ? t(
+ 'cp.rewardMedalNote',
+ '(Permanent medal after getting 3 times)'
+ )
+ : '',
+ },
+ {
+ displayName: t(
+ 'cp.rewardHonorBanner7',
+ 'Honor banner*7 days'
+ ),
+ },
+ {
+ displayName: t(
+ 'cp.rewardHeadwear7',
+ 'Headwear*7days'
+ ),
+ },
+ { displayName: t('cp.rewardMount7', 'Mount*7days') },
+ {
+ displayName: t(
+ 'cp.rewardCoins20000',
+ 'Coins*20000'
+ ),
+ },
+ { displayName: t('cp.rewardDuke7', 'Duke*7days') },
+ ],
+ };
+ }
+ );
+ activityData.statusState = 'ready';
+ activityData.plazaState = 'ready';
+ activityData.enabled = true;
+ serverTimeOffsetMS = 0;
+ countdownEndAt = Date.now() + 2 * 86400000 + 2 * 3600000 + 2 * 60000;
+ }
+
+ function loadData(scope) {
+ var generation = ++requestGeneration;
+ if (isExplicitMockMode()) {
+ applyMockData();
+ render();
+ return Promise.resolve();
+ }
+
+ if (!scope || scope === 'status') {
+ activityData.statusState = 'loading';
+ activityData.rank = [];
+ activityData.previousRows = [];
+ activityData.rewards = [];
+ }
+ // 设计稿的 Plaza 是“单笔 CP 送礼事件流”;当前公开 intimacy-leaderboard 是累计亲密榜,
+ // 两者排序和业务含义不同。在后端事件 feed 上线前保持明确不可用态,绝不把累计榜伪装成送礼记录。
+ activityData.plazaState = 'unavailable';
+ activityData.plazaRows = [];
+ render();
+
+ var statusRequest =
+ window.HyAppAPI && window.HyAppAPI.cpWeeklyRank
+ ? window.HyAppAPI.cpWeeklyRank.status({ limit: 3 })
+ : Promise.reject(new Error('cp_weekly_rank_api_unavailable'));
+
+ // allSettled 统一承接未来可并行加入的 Plaza 事件 feed;generation 防止重试后的旧周榜响应覆盖新状态。
+ return Promise.allSettled([statusRequest]).then(function (results) {
+ if (generation !== requestGeneration) return;
+ if (results[0].status === 'fulfilled') {
+ applyActivityStatus(results[0].value);
+ } else {
+ activityData.statusState = 'error';
+ }
+ render();
+ });
+ }
+
+ function applyTabState() {
+ tabs.forEach(function (tab) {
+ var isActive = tab.getAttribute('data-tab') === activeTab;
+ tab.classList.toggle('is-active', isActive);
+ tab.setAttribute('aria-selected', String(isActive));
+ tab.setAttribute('tabindex', isActive ? '0' : '-1');
+ var image = tab.querySelector('img');
+ if (image) {
+ image.src =
+ ASSET_BASE +
+ (isActive ? 'tab-active.webp' : 'tab-inactive.webp');
+ }
+ });
+ var activeButton = tabs.find(function (tab) {
+ return tab.getAttribute('data-tab') === activeTab;
+ });
+ tabContent.setAttribute(
+ 'aria-labelledby',
+ activeButton ? activeButton.id : 'huwaaCpTabRank'
+ );
+ page.setAttribute('data-tab', activeTab);
+ }
+
+ function getPageWidth() {
+ var viewportWidth =
+ window.innerWidth ||
+ document.documentElement.clientWidth ||
+ DESIGN_WIDTH;
+ return Math.min(Math.max(viewportWidth, 1), MAX_PAGE_WIDTH);
+ }
+
+ function updateStageHeight() {
+ var designHeight = STAGE_HEIGHTS[activeTab] || STAGE_HEIGHTS.rank;
+ // Rewards 的完整 4782px 高度仅对应三档奖励都存在的设计态。加载失败、活动关闭或确实为空时
+ // 只有首块状态面板;按面板底部收拢页面,避免把不存在的二、三档内容伪装成长段空白。
+ if (activeTab === 'rewards' && !activityData.rewards.length) {
+ designHeight =
+ PANEL_LAYOUT.rewardFirstTop +
+ PANEL_LAYOUT.rewardPanelHeight +
+ 160;
+ }
+ var pageWidth = getPageWidth();
+ var designScale = pageWidth / DESIGN_WIDTH;
+ stage.style.height = designHeight + 'px';
+ stage.style.minHeight = designHeight + 'px';
+ document.documentElement.style.setProperty(
+ '--huwaa-cp-page-width',
+ pageWidth + 'px'
+ );
+ document.documentElement.style.setProperty(
+ '--huwaa-cp-design-scale',
+ String(designScale)
+ );
+ stageWrap.style.height = Math.ceil(designHeight * designScale) + 'px';
+ stageWrap.style.minHeight = stageWrap.style.height;
+ copyright.style.top = designHeight - 77 + 'px';
+ document.body.style.minHeight = stageWrap.style.height;
+ viewport.style.minHeight = stageWrap.style.height;
+ page.style.minHeight = stageWrap.style.height;
+ }
+
+ function render() {
+ document.title = t('cp.pageTitle', 'Last Weekly CP');
+ tabContent.innerHTML =
+ activeTab === 'plaza'
+ ? renderPlaza()
+ : activeTab === 'rewards'
+ ? renderRewards()
+ : renderRank();
+ applyTabState();
+ updateStageHeight();
+ updateCountdown();
+ }
+
+ function updateURLTab() {
+ try {
+ var nextURL = new URL(window.location.href);
+ nextURL.searchParams.set('tab', activeTab);
+ window.history.replaceState(null, '', nextURL.toString());
+ } catch (_) {
+ // tab 切换本身不依赖 URL;history 被宿主禁用时仅放弃可分享状态参数。
+ }
+ }
+
+ function selectTab(nextTab, shouldFocus) {
+ if (['plaza', 'rank', 'rewards'].indexOf(nextTab) < 0) return;
+ activeTab = nextTab;
+ updateURLTab();
+ render();
+ var activeButton = tabs.find(function (tab) {
+ return tab.getAttribute('data-tab') === activeTab;
+ });
+ if (shouldFocus && activeButton) activeButton.focus();
+ announcement.textContent =
+ activeButton && activeButton.querySelector('span')
+ ? activeButton.querySelector('span').textContent
+ : '';
+ }
+
+ function bindInteractions() {
+ tabs.forEach(function (tab, index) {
+ tab.addEventListener('click', function () {
+ selectTab(tab.getAttribute('data-tab') || 'rank', false);
+ });
+ tab.addEventListener('keydown', function (event) {
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
+ return;
+ event.preventDefault();
+ var delta = event.key === 'ArrowRight' ? 1 : -1;
+ var nextIndex = (index + delta + tabs.length) % tabs.length;
+ selectTab(tabs[nextIndex].getAttribute('data-tab'), true);
+ });
+ });
+ tabContent.addEventListener('click', function (event) {
+ var retry = event.target.closest('[data-retry]');
+ if (retry) loadData(retry.getAttribute('data-retry') || 'status');
+ });
+ }
+
+ function init() {
+ countdownEndAt = resolveExplicitEndAt();
+ bindInteractions();
+ render();
+ loadData();
+ countdownTimer = window.setInterval(updateCountdown, 1000);
+ window.addEventListener('resize', updateStageHeight);
+ window.addEventListener('hyapp:i18n-ready', function () {
+ if (isExplicitMockMode()) applyMockData();
+ render();
+ });
+ }
+
+ window.addEventListener('beforeunload', function () {
+ requestGeneration += 1;
+ if (countdownTimer) window.clearInterval(countdownTimer);
+ });
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/activity/invite/huwaa.html b/activity/invite/huwaa.html
new file mode 100644
index 0000000..10eca05
--- /dev/null
+++ b/activity/invite/huwaa.html
@@ -0,0 +1,424 @@
+
+
+
+
+
+ Cash Wheel
+
+
+
+
+
+
+
+
+

+

+
+
+
+
+
+
+ Cash Wheel
+
+
+
+
+
+

+
+
+
+
+
+

+
89,094
+
+ ≈$8.9
+
+ Points Left To Withdraw:
+ 1,000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CONGRATULATIONS!
+
+
+ You have won
+
+
+ Medium Reward
+ (500 Points)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ESTIMATED REWARDS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/invite/huwaa/assets/layers/bg-palace-mask.svg b/activity/invite/huwaa/assets/layers/bg-palace-mask.svg
new file mode 100644
index 0000000..458fbff
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/bg-palace-mask.svg
@@ -0,0 +1,11 @@
+
diff --git a/activity/invite/huwaa/assets/layers/bg-palace.webp b/activity/invite/huwaa/assets/layers/bg-palace.webp
new file mode 100644
index 0000000..8ad8add
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/bg-palace.webp differ
diff --git a/activity/invite/huwaa/assets/layers/button-awesome.webp b/activity/invite/huwaa/assets/layers/button-awesome.webp
new file mode 100644
index 0000000..7bbd01f
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/button-awesome.webp differ
diff --git a/activity/invite/huwaa/assets/layers/button-invite.webp b/activity/invite/huwaa/assets/layers/button-invite.webp
new file mode 100644
index 0000000..d009890
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/button-invite.webp differ
diff --git a/activity/invite/huwaa/assets/layers/button-withdraw.webp b/activity/invite/huwaa/assets/layers/button-withdraw.webp
new file mode 100644
index 0000000..11d7c2f
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/button-withdraw.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-blue.webp b/activity/invite/huwaa/assets/layers/chest-blue.webp
new file mode 100644
index 0000000..f57704c
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-blue.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-gold.webp b/activity/invite/huwaa/assets/layers/chest-gold.webp
new file mode 100644
index 0000000..b9e1cbd
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-gold.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-green.webp b/activity/invite/huwaa/assets/layers/chest-green.webp
new file mode 100644
index 0000000..49f9798
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-green.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-orange.webp b/activity/invite/huwaa/assets/layers/chest-orange.webp
new file mode 100644
index 0000000..bfde91b
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-orange.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-purple.webp b/activity/invite/huwaa/assets/layers/chest-purple.webp
new file mode 100644
index 0000000..8cda74c
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-purple.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-red.webp b/activity/invite/huwaa/assets/layers/chest-red.webp
new file mode 100644
index 0000000..ccc2561
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-red.webp differ
diff --git a/activity/invite/huwaa/assets/layers/chest-white-gold.webp b/activity/invite/huwaa/assets/layers/chest-white-gold.webp
new file mode 100644
index 0000000..c5734f9
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/chest-white-gold.webp differ
diff --git a/activity/invite/huwaa/assets/layers/icon-convert.svg b/activity/invite/huwaa/assets/layers/icon-convert.svg
new file mode 100644
index 0000000..f82936a
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/icon-convert.svg
@@ -0,0 +1,10 @@
+
diff --git a/activity/invite/huwaa/assets/layers/icon-finance.svg b/activity/invite/huwaa/assets/layers/icon-finance.svg
new file mode 100644
index 0000000..089ab90
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/icon-finance.svg
@@ -0,0 +1,5 @@
+
diff --git a/activity/invite/huwaa/assets/layers/icon-infinity.svg b/activity/invite/huwaa/assets/layers/icon-infinity.svg
new file mode 100644
index 0000000..df2ca28
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/icon-infinity.svg
@@ -0,0 +1,10 @@
+
diff --git a/activity/invite/huwaa/assets/layers/manifest.json b/activity/invite/huwaa/assets/layers/manifest.json
new file mode 100644
index 0000000..b56363c
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/manifest.json
@@ -0,0 +1,430 @@
+{
+ "schemaVersion": 1,
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "canvas": {
+ "id": "543:1998",
+ "name": "Huwaa运营"
+ },
+ "entryFrames": {
+ "default": "850:232",
+ "win": "850:431",
+ "estimatedRewards": "850:638"
+ },
+ "designSize": {
+ "width": 375,
+ "height": 1114
+ },
+ "forbiddenAggregateNodes": [
+ "543:1998",
+ "850:232",
+ "850:431",
+ "850:638",
+ "850:237",
+ "850:255",
+ "850:262",
+ "850:291",
+ "850:314",
+ "850:425",
+ "850:628",
+ "850:835"
+ ]
+ },
+ "assets": [
+ {
+ "file": "bg-palace-mask.svg",
+ "sourceNode": "850:234",
+ "sourceName": "image 5044",
+ "sourceType": "ROUNDED_RECTANGLE",
+ "purpose": "Bottom fade mask for the palace-only background layer",
+ "bounds": { "width": 541, "height": 812 },
+ "original": { "width": 541, "height": 812 },
+ "exportFormat": "svg",
+ "bytes": 542,
+ "sha256": "f99c7cf545fa81ef21174ea786fa32485a2efa0bc0e0b223b04f13e29255c8cf",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "bg-palace.webp",
+ "sourceNode": "850:235",
+ "sourceNodes": ["850:235", "850:236"],
+ "sourceName": "image 5046",
+ "sourceType": "ROUNDED_RECTANGLE",
+ "purpose": "Palace-only decorative background reused by both half-opacity Figma layers",
+ "bounds": { "width": 541, "height": 812 },
+ "original": { "width": 1024, "height": 1536 },
+ "exportFormat": "webp-lossless",
+ "bytes": 2406896,
+ "sha256": "fb80cada233c7ebcf37aa5a0fa16c92f836db8aa7a05e7b8782426273563ddb6",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "button-awesome.webp",
+ "sourceNode": "850:636",
+ "sourceName": "图层 1 9",
+ "sourceType": "RECTANGLE",
+ "purpose": "Cropped AWESOME button skin without its DOM label",
+ "bounds": { "width": 250, "height": 53 },
+ "original": { "width": 250, "height": 53 },
+ "exportFormat": "webp-lossless",
+ "bytes": 13646,
+ "sha256": "a40f8c77ed9f78921e857dc530e032997d964e396207cff931934e46f9f93888",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "button-invite.webp",
+ "sourceNode": "850:426",
+ "sourceName": "图层 0 315",
+ "sourceType": "RECTANGLE",
+ "purpose": "Invite CTA skin without its DOM label",
+ "bounds": { "width": 280, "height": 50 },
+ "original": { "width": 1990, "height": 360 },
+ "exportFormat": "webp-lossless",
+ "bytes": 417966,
+ "sha256": "538d9833e1e8f8bf7bfbfbf7c6032afa18425a9fff0a661dd83d09f082b1f00b",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "button-withdraw.webp",
+ "sourceNode": "850:312",
+ "sourceName": "图层 1 7",
+ "sourceType": "RECTANGLE",
+ "purpose": "Wallet withdrawal button skin without its DOM label",
+ "bounds": { "width": 214, "height": 34 },
+ "original": { "width": 2055, "height": 329 },
+ "exportFormat": "webp-lossless",
+ "bytes": 374810,
+ "sha256": "e566ebfbe19d15b667a3238df600a7cc1225d3e7ec35903443f82008760239f9",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-blue.webp",
+ "sourceNode": "850:267",
+ "sourceName": "03 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Blue wheel reward chest",
+ "bounds": { "width": 57, "height": 55 },
+ "original": { "width": 354, "height": 336 },
+ "exportFormat": "webp-lossless",
+ "bytes": 149466,
+ "sha256": "0551c01bc09d311d20a8a6f1323629b7bb3898d446b3b22c6e947a2c0b04b272",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-gold.webp",
+ "sourceNode": "850:270",
+ "sourceName": "01 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Gold wheel reward chest",
+ "bounds": { "width": 57, "height": 51 },
+ "original": { "width": 355, "height": 316 },
+ "exportFormat": "webp-lossless",
+ "bytes": 142820,
+ "sha256": "0ba668e4312b24377ead80d73b0fb87bd778dcaf8bf444a9a176e703fe88cd36",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-green.webp",
+ "sourceNode": "850:283",
+ "sourceName": "04 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Green wheel reward chest",
+ "bounds": { "width": 57, "height": 53 },
+ "original": { "width": 354, "height": 331 },
+ "exportFormat": "webp-lossless",
+ "bytes": 164498,
+ "sha256": "5b3e74a47979ac5f4459a4de0ceb66473d272fbb0484f9aa84807a0c630b3209",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-orange.webp",
+ "sourceNode": "850:279",
+ "sourceName": "07 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Orange wheel reward chest",
+ "bounds": { "width": 57, "height": 53 },
+ "original": { "width": 354, "height": 325 },
+ "exportFormat": "webp-lossless",
+ "bytes": 178720,
+ "sha256": "3e7ffe20e4309dee33cbde633597794f68a77e3b07beaff4d5e63f830dbb33e9",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-purple.webp",
+ "sourceNode": "850:276",
+ "sourceName": "05 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Purple wheel reward chest",
+ "bounds": { "width": 57, "height": 55 },
+ "original": { "width": 354, "height": 334 },
+ "exportFormat": "webp-lossless",
+ "bytes": 162400,
+ "sha256": "f6f782ee6440d7349fabf6955f81d71345f71a70a9d3151a20417cb40893703e",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-red.webp",
+ "sourceNode": "850:273",
+ "sourceNodes": ["850:273", "850:429", "850:430"],
+ "sourceName": "02 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Red wheel reward chest reused by the estimated-rewards entry",
+ "bounds": { "width": 57, "height": 52 },
+ "original": { "width": 353, "height": 318 },
+ "exportFormat": "webp-lossless",
+ "bytes": 153572,
+ "sha256": "89fba58f5b4ad0aebf4fe193eabbec83070eeb6246f11f5fa41e580f150e4e39",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "chest-white-gold.webp",
+ "sourceNode": "850:286",
+ "sourceName": "图层 1 6",
+ "sourceType": "RECTANGLE",
+ "purpose": "White-gold wheel reward chest",
+ "bounds": { "width": 57, "height": 55 },
+ "original": { "width": 355, "height": 341 },
+ "exportFormat": "webp-lossless",
+ "bytes": 174792,
+ "sha256": "ed3f81ff4f21ad9a8ca3d32c42a11953b5a2e8bf9bd9ddeb1918d64a4220946b",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "icon-convert.svg",
+ "sourceNode": "850:306",
+ "sourceName": "转换_转换_copy 1",
+ "sourceType": "INSTANCE",
+ "purpose": "Points conversion arrows",
+ "bounds": { "width": 12.521, "height": 14 },
+ "original": { "width": 14, "height": 12.5205 },
+ "exportFormat": "svg",
+ "bytes": 2095,
+ "sha256": "df2b2c6f97d7ae52bd61240846a9168281d2406b61bc1a39b5676061a6484ebb",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "icon-finance.svg",
+ "sourceNode": "850:308",
+ "sourceName": "Finance 1",
+ "sourceType": "INSTANCE",
+ "purpose": "Wallet finance icon",
+ "bounds": { "width": 14, "height": 14 },
+ "original": { "width": 14, "height": 14 },
+ "exportFormat": "svg",
+ "bytes": 2884,
+ "sha256": "86e054e618747a8d6ec1c248fc74bc1781eeb96654cceacc4b4274db24c07be4",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "icon-infinity.svg",
+ "sourceNode": "850:304",
+ "sourceName": "符号_无限 1",
+ "sourceType": "INSTANCE",
+ "purpose": "Infinite points indicator",
+ "bounds": { "width": 14, "height": 14 },
+ "original": { "width": 14, "height": 14 },
+ "exportFormat": "svg",
+ "bytes": 1472,
+ "sha256": "58b7569f3a7f018c492bcfb1633d58c73b4fc807cb9e4a86ff5a03845dd2f841",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "nav-back.svg",
+ "sourceNode": "850:257",
+ "sourceName": "icon_20_返回",
+ "sourceType": "INSTANCE",
+ "purpose": "Navigation back icon",
+ "bounds": { "width": 24, "height": 24 },
+ "original": { "width": 24, "height": 24 },
+ "exportFormat": "svg",
+ "bytes": 419,
+ "sha256": "50d06f24adb0019fcdfb3c3f5db012808c230cd8c2f06c61b775fcccdbf525d4",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "panel-gold-emerald.webp",
+ "sourceNode": "850:292",
+ "sourceNodes": ["850:292", "850:315", "850:836"],
+ "sourceName": "图层 0 46",
+ "sourceType": "RECTANGLE",
+ "purpose": "Reusable gold-and-emerald panel skin reconstructed without Figma one-pixel slices",
+ "bounds": { "width": 351, "height": 205 },
+ "original": { "width": 1502, "height": 879 },
+ "exportFormat": "webp-lossless",
+ "bytes": 722510,
+ "sha256": "6780e103ca19992a5fa3b1e6be5f0d598dab26d803fb9463f9e01fc2fce49789",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "points-a.webp",
+ "sourceNode": "850:297",
+ "sourceNodes": ["850:297", "850:302"],
+ "sourceName": "图层 0 47",
+ "sourceType": "RECTANGLE",
+ "purpose": "Huwaa A points icon",
+ "bounds": { "width": 23, "height": 24 },
+ "original": { "width": 868, "height": 892 },
+ "exportFormat": "webp-lossless",
+ "bytes": 514910,
+ "sha256": "5e3b941cc0f6224ccb17017fad201a508d96230ff70d3f1268d2765636eb2240",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "reward-calendar-check.webp",
+ "sourceNode": "850:394",
+ "sourceNodes": ["850:394", "850:405", "850:416", "850:1085"],
+ "sourceName": "image 5049",
+ "sourceType": "RECTANGLE",
+ "purpose": "Task icon and reward-list fallback icon",
+ "bounds": { "width": 42, "height": 45 },
+ "original": { "width": 658, "height": 701 },
+ "exportFormat": "webp-lossless",
+ "bytes": 182252,
+ "sha256": "79b06b912c2e64ff3487bf8d35b848f493df4134583ba16c34d35ce0427ba81b",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "reward-modal-panel.webp",
+ "sourceNode": "850:629",
+ "sourceName": "图层 0 316",
+ "sourceType": "RECTANGLE",
+ "purpose": "Win dialog decorative panel without reward media or text",
+ "bounds": { "width": 327, "height": 415 },
+ "original": { "width": 994, "height": 1262 },
+ "exportFormat": "webp-lossless",
+ "bytes": 960078,
+ "sha256": "26ded67cb3b3f917d68e517627a3328dbb02c223f3590f2ac3ad9609e510627f",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "status-battery.svg",
+ "sourceNode": "850:240",
+ "sourceName": "Battery",
+ "sourceType": "INSTANCE",
+ "purpose": "Design status-bar battery icon",
+ "bounds": { "width": 24.33, "height": 11.3333 },
+ "original": { "width": 24.33, "height": 11.3333 },
+ "exportFormat": "svg",
+ "bytes": 1005,
+ "sha256": "62fd408926ad465cc16e1fb57634dc5ff2205b4ab11a6b0edc110440e83f0f2b",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "status-cellular.svg",
+ "sourceNode": "850:248",
+ "sourceName": "Cellular Connection",
+ "sourceType": "INSTANCE",
+ "purpose": "Design status-bar cellular icon",
+ "bounds": { "width": 17, "height": 11.3334 },
+ "original": { "width": 17.0001, "height": 11.3334 },
+ "exportFormat": "svg",
+ "bytes": 1147,
+ "sha256": "112f067ba1b97345f39df94cba9e080a46606a4387fe913344a2b2e7e7aee13e",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "status-wifi.svg",
+ "sourceNode": "850:244",
+ "sourceName": "Wifi",
+ "sourceType": "INSTANCE",
+ "purpose": "Design status-bar Wi-Fi icon",
+ "bounds": { "width": 15.3333, "height": 11.01 },
+ "original": { "width": 15.3333, "height": 11.01 },
+ "exportFormat": "svg",
+ "bytes": 1633,
+ "sha256": "22cf5fae5ae340219fbe1ed699d3a7d1562f6f4993c24df5502c640f907a1ec5",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "wallet-inner-panel.webp",
+ "sourceNode": "850:293",
+ "sourceName": "成v下 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Wallet inner texture behind live balance values",
+ "bounds": { "width": 327, "height": 120 },
+ "original": { "width": 1464, "height": 622 },
+ "exportFormat": "webp-lossless",
+ "bytes": 459290,
+ "sha256": "e8fe298ba89673bb8058668481dee7023b30e4445f83ae3363d8202796750f1c",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "wheel-base.webp",
+ "sourceNode": "850:264",
+ "sourceName": "图层 0 49",
+ "sourceType": "RECTANGLE",
+ "purpose": "Empty wheel frame, stage, and pedestal without prizes or controls",
+ "bounds": { "width": 370, "height": 437.464 },
+ "original": { "width": 1038, "height": 1227 },
+ "exportFormat": "webp-lossless",
+ "bytes": 1118800,
+ "sha256": "abb8c248b21d09716eb7022aa991fd18c0452ec7a2f832792104dbeb533b58bb",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ },
+ {
+ "file": "wheel-spin-center.webp",
+ "sourceNode": "850:265",
+ "sourceName": "图层 0 43",
+ "sourceType": "RECTANGLE",
+ "purpose": "Wheel center and pointer skin without Spin or cost text",
+ "bounds": { "width": 106, "height": 117 },
+ "original": { "width": 756, "height": 840 },
+ "exportFormat": "webp-lossless",
+ "bytes": 407758,
+ "sha256": "09659e624c1cc7979ca847142f00e5c322653ad531f7752878d7fe9cc90c6506",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false
+ }
+ ]
+}
diff --git a/activity/invite/huwaa/assets/layers/nav-back.svg b/activity/invite/huwaa/assets/layers/nav-back.svg
new file mode 100644
index 0000000..d2935f3
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/nav-back.svg
@@ -0,0 +1,9 @@
+
diff --git a/activity/invite/huwaa/assets/layers/panel-gold-emerald.webp b/activity/invite/huwaa/assets/layers/panel-gold-emerald.webp
new file mode 100644
index 0000000..bff3714
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/panel-gold-emerald.webp differ
diff --git a/activity/invite/huwaa/assets/layers/points-a.webp b/activity/invite/huwaa/assets/layers/points-a.webp
new file mode 100644
index 0000000..230d10e
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/points-a.webp differ
diff --git a/activity/invite/huwaa/assets/layers/reward-calendar-check.webp b/activity/invite/huwaa/assets/layers/reward-calendar-check.webp
new file mode 100644
index 0000000..2e287dd
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/reward-calendar-check.webp differ
diff --git a/activity/invite/huwaa/assets/layers/reward-modal-panel.webp b/activity/invite/huwaa/assets/layers/reward-modal-panel.webp
new file mode 100644
index 0000000..eb6637d
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/reward-modal-panel.webp differ
diff --git a/activity/invite/huwaa/assets/layers/status-battery.svg b/activity/invite/huwaa/assets/layers/status-battery.svg
new file mode 100644
index 0000000..4b0c570
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/status-battery.svg
@@ -0,0 +1,7 @@
+
diff --git a/activity/invite/huwaa/assets/layers/status-cellular.svg b/activity/invite/huwaa/assets/layers/status-cellular.svg
new file mode 100644
index 0000000..9a5e1ff
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/status-cellular.svg
@@ -0,0 +1,3 @@
+
diff --git a/activity/invite/huwaa/assets/layers/status-wifi.svg b/activity/invite/huwaa/assets/layers/status-wifi.svg
new file mode 100644
index 0000000..4ee1faa
--- /dev/null
+++ b/activity/invite/huwaa/assets/layers/status-wifi.svg
@@ -0,0 +1,3 @@
+
diff --git a/activity/invite/huwaa/assets/layers/wallet-inner-panel.webp b/activity/invite/huwaa/assets/layers/wallet-inner-panel.webp
new file mode 100644
index 0000000..b049dba
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/wallet-inner-panel.webp differ
diff --git a/activity/invite/huwaa/assets/layers/wheel-base.webp b/activity/invite/huwaa/assets/layers/wheel-base.webp
new file mode 100644
index 0000000..70bb6c0
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/wheel-base.webp differ
diff --git a/activity/invite/huwaa/assets/layers/wheel-spin-center.webp b/activity/invite/huwaa/assets/layers/wheel-spin-center.webp
new file mode 100644
index 0000000..674f4f7
Binary files /dev/null and b/activity/invite/huwaa/assets/layers/wheel-spin-center.webp differ
diff --git a/activity/invite/huwaa/huwaa.css b/activity/invite/huwaa/huwaa.css
new file mode 100644
index 0000000..528b480
--- /dev/null
+++ b/activity/invite/huwaa/huwaa.css
@@ -0,0 +1,1013 @@
+:root {
+ --huwaa-page-width: min(100vw, 430px);
+ --huwaa-unit: calc(var(--huwaa-page-width) / 375);
+ --huwaa-bg: #07211f;
+ --huwaa-panel: #043c2b;
+ --huwaa-green: #259c72;
+ --huwaa-gold: #fbbf24;
+ --huwaa-gold-light: #ffce5c;
+ --huwaa-white: #fff;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ min-height: 100%;
+ margin: 0;
+ background: #020b0a;
+ color: var(--huwaa-white);
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text',
+ 'Helvetica Neue', Arial, sans-serif;
+}
+
+button {
+ margin: 0;
+ border: 0;
+ padding: 0;
+ color: inherit;
+ font: inherit;
+ background: transparent;
+ -webkit-tap-highlight-color: transparent;
+}
+
+button:focus-visible {
+ outline: calc(2 * var(--huwaa-unit)) solid #fff;
+ outline-offset: calc(2 * var(--huwaa-unit));
+}
+
+img {
+ display: block;
+ max-width: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.huwaa-page {
+ position: relative;
+ width: var(--huwaa-page-width);
+ min-height: calc(1114 * var(--huwaa-unit));
+ margin: 0 auto;
+ overflow: hidden;
+ background: var(--huwaa-bg);
+}
+
+.huwaa-stage {
+ position: relative;
+ width: 100%;
+ height: calc(1114 * var(--huwaa-unit));
+ overflow: hidden;
+ background: var(--huwaa-bg);
+ isolation: isolate;
+}
+
+.huwaa-palace {
+ position: absolute;
+ z-index: 0;
+ top: calc(28 * var(--huwaa-unit));
+ left: calc(-83 * var(--huwaa-unit));
+ width: calc(541 * var(--huwaa-unit));
+ height: calc(812 * var(--huwaa-unit));
+ -webkit-mask: url('./assets/layers/bg-palace-mask.svg') center / 100% 100%
+ no-repeat;
+ mask: url('./assets/layers/bg-palace-mask.svg') center / 100% 100% no-repeat;
+}
+
+.huwaa-palace img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0.5;
+ object-fit: cover;
+ mix-blend-mode: screen;
+}
+
+.huwaa-status-bar {
+ position: absolute;
+ z-index: 12;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: calc(44 * var(--huwaa-unit));
+ background: #000;
+}
+
+.huwaa-status-time {
+ position: absolute;
+ top: calc(13 * var(--huwaa-unit));
+ left: calc(33.87 * var(--huwaa-unit));
+ width: calc(54 * var(--huwaa-unit));
+ height: calc(15 * var(--huwaa-unit));
+ color: #fff;
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: calc(15 * var(--huwaa-unit));
+ font-weight: 400;
+ line-height: calc(15 * var(--huwaa-unit));
+ text-align: center;
+}
+
+.huwaa-status-cellular {
+ position: absolute;
+ top: calc(15 * var(--huwaa-unit));
+ left: calc(294.34 * var(--huwaa-unit));
+ width: calc(17 * var(--huwaa-unit));
+ height: calc(11.333 * var(--huwaa-unit));
+}
+
+.huwaa-status-wifi {
+ position: absolute;
+ top: calc(16.33 * var(--huwaa-unit));
+ left: calc(316.34 * var(--huwaa-unit));
+ width: calc(15.333 * var(--huwaa-unit));
+ height: calc(11.01 * var(--huwaa-unit));
+}
+
+.huwaa-status-battery {
+ position: absolute;
+ top: calc(16.34 * var(--huwaa-unit));
+ left: calc(336.67 * var(--huwaa-unit));
+ width: calc(24.33 * var(--huwaa-unit));
+ height: calc(11.333 * var(--huwaa-unit));
+}
+
+.huwaa-nav {
+ position: absolute;
+ z-index: 11;
+ top: calc(44 * var(--huwaa-unit));
+ left: 0;
+ width: 100%;
+ height: calc(44 * var(--huwaa-unit));
+ background: #000;
+}
+
+.huwaa-nav h1 {
+ position: absolute;
+ top: calc(10 * var(--huwaa-unit));
+ left: 50%;
+ margin: 0;
+ transform: translateX(-50%);
+ color: #fff;
+ font-size: calc(18 * var(--huwaa-unit));
+ font-weight: 500;
+ line-height: calc(18 * var(--huwaa-unit));
+ white-space: nowrap;
+}
+
+.huwaa-nav-back {
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ left: calc(6 * var(--huwaa-unit));
+ width: calc(44 * var(--huwaa-unit));
+ height: calc(44 * var(--huwaa-unit));
+ cursor: pointer;
+}
+
+.huwaa-nav-back img {
+ position: absolute;
+ top: calc(10 * var(--huwaa-unit));
+ left: calc(10 * var(--huwaa-unit));
+ width: calc(24 * var(--huwaa-unit));
+ height: calc(24 * var(--huwaa-unit));
+}
+
+.huwaa-wallet-card {
+ position: absolute;
+ z-index: 4;
+ top: calc(111 * var(--huwaa-unit));
+ left: calc(12 * var(--huwaa-unit));
+ width: calc(351 * var(--huwaa-unit));
+ height: calc(205 * var(--huwaa-unit));
+}
+
+.huwaa-wallet-skin {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.huwaa-wallet-texture {
+ position: absolute;
+ top: calc(32 * var(--huwaa-unit));
+ left: calc(12 * var(--huwaa-unit));
+ width: calc(327 * var(--huwaa-unit));
+ height: calc(120 * var(--huwaa-unit));
+ overflow: hidden;
+ border: calc(1 * var(--huwaa-unit)) solid #facd61;
+ border-radius: calc(8 * var(--huwaa-unit));
+}
+
+.huwaa-wallet-texture img {
+ position: absolute;
+ top: -8.5%;
+ left: -0.29%;
+ width: 100.58%;
+ height: 117%;
+}
+
+.huwaa-wallet-rate {
+ position: absolute;
+ top: calc(45 * var(--huwaa-unit));
+ left: calc(120 * var(--huwaa-unit));
+ display: flex;
+ height: calc(16 * var(--huwaa-unit));
+ align-items: center;
+ gap: calc(4 * var(--huwaa-unit));
+}
+
+.huwaa-wallet-rate > img:first-child {
+ width: calc(14 * var(--huwaa-unit));
+ height: calc(15 * var(--huwaa-unit));
+ object-fit: contain;
+}
+
+.huwaa-wallet-rate strong {
+ color: #fff;
+ font-size: calc(14 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+}
+
+.huwaa-wallet-rate .huwaa-rate-exchange {
+ width: calc(14 * var(--huwaa-unit));
+ height: calc(12.521 * var(--huwaa-unit));
+ margin-left: calc(20 * var(--huwaa-unit));
+ transform: rotate(90deg);
+}
+
+.huwaa-wallet-rate .huwaa-rate-finance,
+.huwaa-wallet-rate .huwaa-rate-infinity {
+ width: calc(14 * var(--huwaa-unit));
+ height: calc(14 * var(--huwaa-unit));
+}
+
+.huwaa-wallet-balance {
+ position: absolute;
+ top: calc(67 * var(--huwaa-unit));
+ left: calc(80 * var(--huwaa-unit));
+ display: flex;
+ align-items: center;
+}
+
+.huwaa-wallet-balance img {
+ width: calc(23 * var(--huwaa-unit));
+ height: calc(24 * var(--huwaa-unit));
+ margin-right: calc(5 * var(--huwaa-unit));
+ object-fit: contain;
+}
+
+.huwaa-wallet-balance strong {
+ color: #fff;
+ font-size: calc(32 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.huwaa-wallet-cash {
+ position: absolute;
+ top: calc(87 * var(--huwaa-unit));
+ left: calc(230 * var(--huwaa-unit));
+ color: var(--huwaa-gold);
+ font-size: calc(14 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.huwaa-wallet-remaining {
+ position: absolute;
+ top: calc(112 * var(--huwaa-unit));
+ left: 50%;
+ margin: 0;
+ transform: translateX(-50%);
+ font-size: calc(10 * var(--huwaa-unit));
+ font-weight: 600;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.huwaa-wallet-remaining span {
+ color: #8cdfab;
+}
+
+.huwaa-wallet-remaining strong {
+ color: var(--huwaa-gold);
+ font-weight: 600;
+}
+
+.huwaa-withdraw-button {
+ position: absolute;
+ top: calc(161 * var(--huwaa-unit));
+ left: calc(69 * var(--huwaa-unit));
+ width: calc(214 * var(--huwaa-unit));
+ height: calc(34 * var(--huwaa-unit));
+ cursor: pointer;
+}
+
+.huwaa-withdraw-button::before {
+ position: absolute;
+ inset: calc(-5 * var(--huwaa-unit)) 0;
+ content: '';
+}
+
+.huwaa-withdraw-button img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.huwaa-withdraw-button span {
+ position: absolute;
+ inset: calc(7 * var(--huwaa-unit)) 0 auto;
+ color: var(--huwaa-gold-light);
+ font-size: calc(14 * var(--huwaa-unit));
+ font-weight: 600;
+ line-height: 1.4;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.huwaa-wheel-region {
+ position: absolute;
+ z-index: 3;
+ top: calc(331 * var(--huwaa-unit));
+ left: 0;
+ width: calc(375 * var(--huwaa-unit));
+ height: calc(468 * var(--huwaa-unit));
+}
+
+.huwaa-wheel-base {
+ position: absolute;
+ top: calc(18.768 * var(--huwaa-unit));
+ left: calc(2.5 * var(--huwaa-unit));
+ width: calc(370 * var(--huwaa-unit));
+ height: calc(437.464 * var(--huwaa-unit));
+}
+
+.huwaa-reward-entry {
+ position: absolute;
+ z-index: 6;
+ top: calc(-3 * var(--huwaa-unit));
+ left: calc(308 * var(--huwaa-unit));
+ width: calc(44 * var(--huwaa-unit));
+ height: calc(44 * var(--huwaa-unit));
+ cursor: pointer;
+}
+
+.huwaa-reward-entry img {
+ position: absolute;
+ top: calc(3 * var(--huwaa-unit));
+ left: calc(1 * var(--huwaa-unit));
+ width: calc(42 * var(--huwaa-unit));
+ height: calc(38 * var(--huwaa-unit));
+ object-fit: contain;
+}
+
+.huwaa-reward-entry img:last-child {
+ mix-blend-mode: screen;
+}
+
+.huwaa-wheel-prizes {
+ position: absolute;
+ inset: 0;
+}
+
+.huwaa-prize {
+ position: absolute;
+ z-index: 4;
+ width: calc(57 * var(--huwaa-unit));
+ height: calc(79 * var(--huwaa-unit));
+ cursor: pointer;
+ transition:
+ filter 120ms ease,
+ transform 120ms ease;
+}
+
+.huwaa-prize img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: calc(57 * var(--huwaa-unit));
+ object-fit: contain;
+}
+
+.huwaa-prize span {
+ position: absolute;
+ top: calc(49 * var(--huwaa-unit));
+ left: calc(4 * var(--huwaa-unit));
+ width: calc(44 * var(--huwaa-unit));
+ height: calc(26 * var(--huwaa-unit));
+ overflow: visible;
+ color: #ba3320;
+ font-size: calc(11.187 * var(--huwaa-unit));
+ font-weight: 600;
+ line-height: calc(25.171 * var(--huwaa-unit));
+ text-align: center;
+ white-space: nowrap;
+}
+
+.huwaa-prize.is-active {
+ z-index: 5;
+ filter: brightness(1.45)
+ drop-shadow(0 0 calc(7 * var(--huwaa-unit)) #fff09a);
+ transform: scale(1.08);
+}
+
+.huwaa-prize[data-index='0'] {
+ top: calc(68 * var(--huwaa-unit));
+ left: calc(159 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='0'] img,
+.huwaa-prize[data-index='1'] img {
+ height: calc(53 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='1'] {
+ top: calc(103 * var(--huwaa-unit));
+ left: calc(74 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='2'] {
+ top: calc(108 * var(--huwaa-unit));
+ left: calc(241 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='2'] img,
+.huwaa-prize[data-index='3'] img,
+.huwaa-prize[data-index='4'] img {
+ height: calc(55 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='3'] {
+ top: calc(196 * var(--huwaa-unit));
+ left: calc(55 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='4'] {
+ top: calc(196 * var(--huwaa-unit));
+ left: calc(265 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='5'] {
+ top: calc(268 * var(--huwaa-unit));
+ left: calc(119 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='5'] img {
+ height: calc(52 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='6'] {
+ top: calc(267 * var(--huwaa-unit));
+ left: calc(201 * var(--huwaa-unit));
+}
+
+.huwaa-prize[data-index='6'] img {
+ height: calc(51 * var(--huwaa-unit));
+}
+
+.huwaa-spin-button {
+ position: absolute;
+ z-index: 7;
+ top: calc(146 * var(--huwaa-unit));
+ left: calc(135 * var(--huwaa-unit));
+ width: calc(106 * var(--huwaa-unit));
+ height: calc(117 * var(--huwaa-unit));
+ cursor: pointer;
+}
+
+.huwaa-spin-button > img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.huwaa-spin-label {
+ position: absolute;
+ top: calc(40 * var(--huwaa-unit));
+ left: 50%;
+ width: calc(74 * var(--huwaa-unit));
+ transform: translateX(-50%);
+ color: #ba3320;
+ font-size: calc(22.375 * var(--huwaa-unit));
+ font-weight: 600;
+ line-height: calc(26 * var(--huwaa-unit));
+ text-align: center;
+}
+
+.huwaa-spin-cost {
+ position: absolute;
+ top: calc(76 * var(--huwaa-unit));
+ left: 50%;
+ display: flex;
+ width: calc(38 * var(--huwaa-unit));
+ height: calc(24 * var(--huwaa-unit));
+ align-items: center;
+ justify-content: center;
+ transform: translateX(-50%);
+ border: calc(1.02 * var(--huwaa-unit)) solid #fb7185;
+ border-radius: calc(11.735 * var(--huwaa-unit));
+ color: #fff;
+ font-size: calc(12.246 * var(--huwaa-unit));
+ font-weight: 600;
+ line-height: calc(14.286 * var(--huwaa-unit));
+ background: #e11d48;
+}
+
+.huwaa-spin-button[disabled] {
+ cursor: wait;
+}
+
+.huwaa-invite-button {
+ position: absolute;
+ z-index: 8;
+ top: calc(749 * var(--huwaa-unit));
+ left: calc(48 * var(--huwaa-unit));
+ width: calc(280 * var(--huwaa-unit));
+ height: calc(50 * var(--huwaa-unit));
+ cursor: pointer;
+}
+
+.huwaa-invite-button img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.huwaa-invite-button span {
+ position: absolute;
+ top: calc(14 * var(--huwaa-unit));
+ left: 0;
+ width: 100%;
+ color: #501b00;
+ font-size: calc(16 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.huwaa-extendable-panel {
+ position: absolute;
+ overflow: hidden;
+}
+
+.huwaa-extendable-skin {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.huwaa-skin-top,
+.huwaa-skin-middle,
+.huwaa-skin-bottom {
+ position: absolute;
+ left: 0;
+ width: 100%;
+}
+
+.huwaa-skin-top {
+ top: 0;
+ height: calc(117 * var(--huwaa-unit));
+ background: url('./assets/layers/panel-gold-emerald.webp') top center / 100%
+ auto no-repeat;
+}
+
+.huwaa-skin-middle {
+ top: calc(117 * var(--huwaa-unit));
+ bottom: calc(91.538 * var(--huwaa-unit));
+ background: linear-gradient(
+ 90deg,
+ #011f14 0%,
+ #02261b 6%,
+ #033a26 27%,
+ #064a2f 50%,
+ #043724 74%,
+ #02261a 94%,
+ #011f14 100%
+ );
+ box-shadow:
+ inset calc(3 * var(--huwaa-unit)) 0 #e5a938,
+ inset calc(-3 * var(--huwaa-unit)) 0 #e5a938;
+}
+
+.huwaa-skin-bottom {
+ bottom: 0;
+ height: calc(91.538 * var(--huwaa-unit));
+ background: url('./assets/layers/panel-gold-emerald.webp') bottom center /
+ 100% auto no-repeat;
+}
+
+.huwaa-task-panel {
+ z-index: 5;
+ top: calc(810 * var(--huwaa-unit));
+ left: calc(9 * var(--huwaa-unit));
+ width: calc(357 * var(--huwaa-unit));
+ height: calc(282.504 * var(--huwaa-unit));
+}
+
+.huwaa-task-list {
+ position: absolute;
+ z-index: 1;
+ top: calc(31 * var(--huwaa-unit));
+ left: calc(15 * var(--huwaa-unit));
+ display: grid;
+ width: calc(327 * var(--huwaa-unit));
+ gap: calc(12 * var(--huwaa-unit));
+}
+
+.huwaa-task-card {
+ position: relative;
+ display: flex;
+ width: 100%;
+ height: calc(69 * var(--huwaa-unit));
+ align-items: center;
+ border: calc(1 * var(--huwaa-unit)) solid var(--huwaa-green);
+ border-radius: calc(7 * var(--huwaa-unit));
+ background: rgba(8, 43, 50, 0.2);
+}
+
+.huwaa-task-icon {
+ position: absolute;
+ top: calc(12 * var(--huwaa-unit));
+ left: calc(12 * var(--huwaa-unit));
+ width: calc(42 * var(--huwaa-unit));
+ height: calc(45 * var(--huwaa-unit));
+ object-fit: cover;
+}
+
+.huwaa-task-copy {
+ position: absolute;
+ top: calc(12 * var(--huwaa-unit));
+ left: calc(62 * var(--huwaa-unit));
+ display: flex;
+ width: calc(167 * var(--huwaa-unit));
+ flex-direction: column;
+ gap: calc(4 * var(--huwaa-unit));
+}
+
+.huwaa-task-copy strong {
+ overflow: hidden;
+ color: #fff;
+ font-size: calc(12 * var(--huwaa-unit));
+ font-weight: 400;
+ line-height: calc(12 * var(--huwaa-unit));
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.huwaa-task-copy span {
+ display: -webkit-box;
+ width: calc(151 * var(--huwaa-unit));
+ overflow: hidden;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: calc(10 * var(--huwaa-unit));
+ font-weight: 400;
+ line-height: calc(10 * var(--huwaa-unit));
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
+.huwaa-task-reward {
+ position: absolute;
+ top: calc(10 * var(--huwaa-unit));
+ right: calc(12 * var(--huwaa-unit));
+ color: var(--huwaa-gold);
+ font-size: calc(14 * var(--huwaa-unit));
+ font-weight: 500;
+ line-height: calc(12 * var(--huwaa-unit));
+}
+
+.huwaa-task-status {
+ position: absolute;
+ right: calc(12 * var(--huwaa-unit));
+ bottom: calc(10 * var(--huwaa-unit));
+ display: flex;
+ min-width: calc(82 * var(--huwaa-unit));
+ height: calc(22 * var(--huwaa-unit));
+ align-items: center;
+ justify-content: center;
+ border: calc(1 * var(--huwaa-unit)) solid #3ce89b;
+ border-radius: calc(34 * var(--huwaa-unit));
+ padding: 0 calc(10 * var(--huwaa-unit));
+ color: #fff;
+ font-size: calc(10 * var(--huwaa-unit));
+ font-weight: 400;
+ line-height: calc(12 * var(--huwaa-unit));
+ background: #143322;
+ box-shadow:
+ inset calc(-1.867 * var(--huwaa-unit)) calc(-1.867 * var(--huwaa-unit))
+ calc(1.867 * var(--huwaa-unit)) rgba(0, 0, 0, 0.2),
+ inset calc(1.867 * var(--huwaa-unit)) calc(1.867 * var(--huwaa-unit))
+ calc(1.867 * var(--huwaa-unit)) rgba(0, 0, 0, 0.2),
+ inset 0 0 calc(9.333 * var(--huwaa-unit)) rgba(47, 182, 122, 0.2),
+ inset 0 0 calc(28 * var(--huwaa-unit)) rgba(96, 219, 164, 0.75),
+ 0 0 calc(4.667 * var(--huwaa-unit)) rgba(47, 182, 122, 0.25);
+}
+
+.huwaa-task-card.is-pending .huwaa-task-status {
+ color: #501b00;
+ border-color: #f6c34e;
+ cursor: pointer;
+ background: #fbbf24;
+ box-shadow: none;
+}
+
+.huwaa-modal-scrim {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ background: rgba(0, 0, 0, 0.4);
+}
+
+.huwaa-win-modal,
+.huwaa-reward-drawer {
+ position: fixed;
+ z-index: 30;
+ top: 0;
+ left: 50%;
+ width: var(--huwaa-page-width);
+ height: min(100dvh, calc(1114 * var(--huwaa-unit)));
+ transform: translateX(-50%);
+}
+
+.huwaa-win-dialog {
+ position: absolute;
+ top: calc(350 * var(--huwaa-unit));
+ left: calc(24 * var(--huwaa-unit));
+ width: calc(327 * var(--huwaa-unit));
+ height: calc(415 * var(--huwaa-unit));
+}
+
+.huwaa-win-skin {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.huwaa-win-media {
+ position: absolute;
+ top: calc(37.5 * var(--huwaa-unit));
+ left: calc(102.03 * var(--huwaa-unit));
+ display: flex;
+ width: calc(132 * var(--huwaa-unit));
+ height: calc(128 * var(--huwaa-unit));
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.huwaa-win-media img {
+ width: calc(112 * var(--huwaa-unit));
+ height: calc(102 * var(--huwaa-unit));
+ object-fit: contain;
+}
+
+.huwaa-win-dialog h2 {
+ position: absolute;
+ top: calc(179 * var(--huwaa-unit));
+ left: 50%;
+ width: calc(312 * var(--huwaa-unit));
+ margin: 0;
+ transform: translateX(-50%);
+ color: #fcf488;
+ font-size: calc(28 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ text-align: center;
+ white-space: nowrap;
+ background: linear-gradient(
+ 180deg,
+ #fcf488 0%,
+ #fce295 49.519%,
+ #ffbe1b 100%
+ );
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ -webkit-text-stroke: calc(1.25 * var(--huwaa-unit)) #351700;
+ paint-order: stroke fill;
+ text-shadow: 0 calc(1 * var(--huwaa-unit)) 0 #fff3a1;
+}
+
+.huwaa-win-kicker {
+ position: absolute;
+ top: calc(233 * var(--huwaa-unit));
+ left: 50%;
+ margin: 0;
+ transform: translateX(-50%);
+ color: #fce295;
+ font-size: calc(16 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ white-space: nowrap;
+ background: linear-gradient(
+ 180deg,
+ #fcf488 0%,
+ #fce295 49.519%,
+ #ffbe1b 100%
+ );
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ -webkit-text-stroke: calc(0.5 * var(--huwaa-unit)) #6f4700;
+}
+
+.huwaa-win-reward {
+ position: absolute;
+ top: calc(260 * var(--huwaa-unit));
+ left: 50%;
+ display: flex;
+ width: calc(253 * var(--huwaa-unit));
+ margin: 0;
+ transform: translateX(-50%);
+ flex-direction: column;
+ color: #025333;
+ font-size: calc(20 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ text-align: center;
+}
+
+.huwaa-win-reward strong,
+.huwaa-win-reward span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.huwaa-awesome-button {
+ position: absolute;
+ top: calc(331 * var(--huwaa-unit));
+ left: calc(37 * var(--huwaa-unit));
+ width: calc(250 * var(--huwaa-unit));
+ height: calc(53 * var(--huwaa-unit));
+ cursor: pointer;
+}
+
+.huwaa-awesome-button img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.huwaa-awesome-button span {
+ position: absolute;
+ top: calc(16 * var(--huwaa-unit));
+ left: 0;
+ width: 100%;
+ color: var(--huwaa-gold-light);
+ font-size: calc(16 * var(--huwaa-unit));
+ font-weight: 600;
+ line-height: 1.4;
+ text-align: center;
+}
+
+.huwaa-reward-drawer .huwaa-modal-scrim {
+ background: rgba(0, 0, 0, 0.4);
+}
+
+.huwaa-reward-sheet {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: calc(375 * var(--huwaa-unit));
+ height: calc(367 * var(--huwaa-unit));
+}
+
+.huwaa-reward-sheet .huwaa-skin-top {
+ height: calc(123 * var(--huwaa-unit));
+}
+
+.huwaa-reward-sheet .huwaa-skin-middle {
+ top: calc(123 * var(--huwaa-unit));
+ bottom: 0;
+}
+
+.huwaa-reward-sheet h2 {
+ position: absolute;
+ z-index: 1;
+ top: calc(34 * var(--huwaa-unit));
+ left: 50%;
+ margin: 0;
+ transform: translateX(-50%);
+ color: var(--huwaa-gold);
+ font-size: calc(20 * var(--huwaa-unit));
+ font-weight: 700;
+ line-height: 1.4;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.huwaa-win-dialog h2:focus,
+.huwaa-reward-sheet h2:focus {
+ outline: none;
+}
+
+.huwaa-estimated-grid {
+ position: absolute;
+ z-index: 1;
+ top: calc(77 * var(--huwaa-unit));
+ left: calc(12 * var(--huwaa-unit));
+ display: grid;
+ width: calc(351 * var(--huwaa-unit));
+ grid-template-columns: repeat(4, calc(71 * var(--huwaa-unit)));
+ column-gap: calc(22.333 * var(--huwaa-unit));
+ row-gap: calc(24 * var(--huwaa-unit));
+ max-height: calc(290 * var(--huwaa-unit));
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding-bottom: calc(12 * var(--huwaa-unit));
+ scrollbar-width: none;
+}
+
+.huwaa-estimated-grid::-webkit-scrollbar {
+ display: none;
+}
+
+.huwaa-estimated-item {
+ position: relative;
+ width: calc(71 * var(--huwaa-unit));
+ height: calc(95 * var(--huwaa-unit));
+}
+
+.huwaa-estimated-icon {
+ display: flex;
+ width: calc(71 * var(--huwaa-unit));
+ height: calc(71 * var(--huwaa-unit));
+ align-items: center;
+ justify-content: center;
+ border: calc(1 * var(--huwaa-unit)) solid var(--huwaa-green);
+ border-radius: 50%;
+ background: rgba(8, 43, 50, 0.2);
+}
+
+.huwaa-estimated-icon img {
+ width: calc(44 * var(--huwaa-unit));
+ height: calc(47 * var(--huwaa-unit));
+ object-fit: cover;
+}
+
+.huwaa-estimated-item strong {
+ position: absolute;
+ top: calc(83 * var(--huwaa-unit));
+ left: 0;
+ width: 100%;
+ overflow: hidden;
+ color: #fff;
+ font-size: calc(12 * var(--huwaa-unit));
+ font-weight: 400;
+ line-height: calc(12 * var(--huwaa-unit));
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.huwaa-stage.is-busy .huwaa-spin-button {
+ pointer-events: none;
+}
+
+html.huwaa-modal-open,
+html.huwaa-modal-open body {
+ overflow: hidden;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .huwaa-prize {
+ transition: none;
+ }
+}
+
+[dir='rtl'] .huwaa-nav-back img {
+ transform: scaleX(-1);
+}
+
+@media (min-width: 431px) {
+ body {
+ padding-bottom: 24px;
+ }
+
+ .huwaa-page {
+ box-shadow: 0 0 40px rgba(0, 0, 0, 0.45);
+ }
+}
diff --git a/activity/invite/huwaa/huwaa.js b/activity/invite/huwaa/huwaa.js
new file mode 100644
index 0000000..9f0fdeb
--- /dev/null
+++ b/activity/invite/huwaa/huwaa.js
@@ -0,0 +1,1009 @@
+(function () {
+ 'use strict';
+
+ var ASSET_BASE = './huwaa/assets/layers/';
+ var params = new URLSearchParams(window.location.search || '');
+ var useMock = params.get('mock') === '1';
+ var DESIGN_PRIZES = [
+ {
+ id: 'medium-orange',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-orange.webp',
+ points: 500,
+ },
+ {
+ id: 'medium-green',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-green.webp',
+ points: 500,
+ },
+ {
+ id: 'medium-purple',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-white-gold.webp',
+ points: 500,
+ },
+ {
+ id: 'medium-teal',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-blue.webp',
+ points: 500,
+ },
+ {
+ id: 'medium-gold',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-purple.webp',
+ points: 500,
+ },
+ {
+ id: 'medium-red',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-red.webp',
+ points: 500,
+ },
+ {
+ id: 'medium-brown',
+ name: 'Medium',
+ image: ASSET_BASE + 'chest-gold.webp',
+ points: 500,
+ },
+ ];
+ var DESIGN_TASKS = [
+ {
+ id: 'microphone-1',
+ title: 'Use the microphone for 1 min',
+ description: 'Friend completes biometric authentication',
+ titleKey: 'huwaa_invite.task_title',
+ descriptionKey: 'huwaa_invite.task_description',
+ reward: 1,
+ completed: true,
+ },
+ {
+ id: 'microphone-2',
+ title: 'Use the microphone for 1 min',
+ description: 'Friend completes biometric authentication',
+ titleKey: 'huwaa_invite.task_title',
+ descriptionKey: 'huwaa_invite.task_description',
+ reward: 1,
+ completed: true,
+ },
+ {
+ id: 'microphone-3',
+ title: 'Use the microphone for 1 min',
+ description: 'Friend completes biometric authentication',
+ titleKey: 'huwaa_invite.task_title',
+ descriptionKey: 'huwaa_invite.task_description',
+ reward: 1,
+ completed: true,
+ },
+ ];
+ var state = {
+ wheelID: params.get('wheel_id') || params.get('wheelId') || 'cash',
+ busy: false,
+ pointBalance: useMock ? 89094 : 0,
+ cashValue: useMock ? 8.9 : 0,
+ ratePoints: '90K',
+ withdrawalRemaining: useMock ? 1000 : 0,
+ drawCost: 5,
+ prizes: DESIGN_PRIZES.slice(),
+ tasks: DESIGN_TASKS.slice(),
+ estimatedRewards: [],
+ inviteCode: '',
+ inviteLink: '',
+ spinCount: 0,
+ };
+
+ function $(selector) {
+ return document.querySelector(selector);
+ }
+
+ function $all(selector) {
+ return Array.prototype.slice.call(document.querySelectorAll(selector));
+ }
+
+ function t(key, fallback) {
+ if (window.HyAppI18n && window.HyAppI18n.t) {
+ return window.HyAppI18n.t(key, fallback);
+ }
+ return fallback || key;
+ }
+
+ function toast(message) {
+ var target = window.HyToast || window.HyAppToast;
+ if (target && target.show) {
+ target.show(message);
+ return;
+ }
+ window.alert(message);
+ }
+
+ function text(value) {
+ return String(
+ value === undefined || value === null ? '' : value
+ ).trim();
+ }
+
+ function number(value, fallback) {
+ var result = Number(value);
+ return Number.isFinite(result) ? result : Number(fallback || 0);
+ }
+
+ function first(source, keys) {
+ source = source || {};
+ for (var index = 0; index < keys.length; index += 1) {
+ var value = source[keys[index]];
+ if (value !== undefined && value !== null && value !== '') {
+ return value;
+ }
+ }
+ return '';
+ }
+
+ function envelope(payload) {
+ if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) {
+ return payload.data;
+ }
+ return payload || {};
+ }
+
+ function parseJSON(value) {
+ if (!value || typeof value !== 'string') return {};
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ return {};
+ }
+ }
+
+ function formatNumber(value) {
+ var lang =
+ window.HyAppI18n && window.HyAppI18n.lang
+ ? window.HyAppI18n.lang()
+ : 'en';
+ return new Intl.NumberFormat(lang).format(number(value));
+ }
+
+ function resolveAssetURL(value) {
+ value = text(value);
+ if (!value) return '';
+ if (
+ /^(https?:)?\/\//i.test(value) ||
+ /^(data|blob):/i.test(value) ||
+ value.charAt(0) === '/' ||
+ value.indexOf('./') === 0 ||
+ value.indexOf('../') === 0
+ ) {
+ return value;
+ }
+ // 旧版资源组只返回 wheel/assets/other 下的裸文件名;统一补齐路径,避免把配置内容误当当前 invite 目录的相对文件。
+ return new URL('../wheel/assets/other/' + value, window.location.href)
+ .href;
+ }
+
+ function normalizeReward(item, fallback, index) {
+ item = item || {};
+ fallback = fallback || DESIGN_PRIZES[index % DESIGN_PRIZES.length];
+ var metadata = parseJSON(
+ first(item, ['metadata_json', 'metadataJson'])
+ );
+ var image =
+ first(metadata, [
+ 'preview_url',
+ 'previewUrl',
+ 'asset_url',
+ 'assetUrl',
+ ]) ||
+ first(item, [
+ 'cover_url',
+ 'coverUrl',
+ 'cover',
+ 'resource_cover',
+ 'resourceCover',
+ 'preview_url',
+ 'previewUrl',
+ 'image_url',
+ 'imageUrl',
+ 'icon',
+ 'avatar',
+ ]);
+ var name =
+ first(item, [
+ 'display_name',
+ 'displayName',
+ 'reward_name',
+ 'rewardName',
+ 'name',
+ ]) || fallback.name;
+ var points = number(
+ first(item, [
+ 'display_value',
+ 'displayValue',
+ 'reward_value',
+ 'rewardValue',
+ 'reward_coins',
+ 'rewardCoins',
+ 'points',
+ 'value',
+ 'amount',
+ ]),
+ fallback.points
+ );
+ return {
+ id:
+ text(
+ first(item, [
+ 'id',
+ 'tier_id',
+ 'tierId',
+ 'reward_id',
+ 'rewardId',
+ 'resource_id',
+ 'resourceId',
+ ])
+ ) || fallback.id,
+ name: text(name) || fallback.name,
+ image: resolveAssetURL(image) || fallback.image,
+ points: points,
+ raw: item,
+ };
+ }
+
+ function rewardItems(payload) {
+ var data = envelope(payload);
+ var values = Array.isArray(data)
+ ? data
+ : data.rewards ||
+ data.tiers ||
+ data.items ||
+ (data.config && (data.config.rewards || data.config.tiers)) ||
+ [];
+ if (!Array.isArray(values)) return [];
+ return values.map(function (item, index) {
+ return normalizeReward(item, DESIGN_PRIZES[index], index);
+ });
+ }
+
+ function normalizeTasks(payload) {
+ var data = envelope(payload);
+ var values =
+ data.tasks ||
+ data.invite_tasks ||
+ data.inviteTasks ||
+ (data.config && data.config.tasks) ||
+ [];
+ if (!Array.isArray(values)) return [];
+ return values.slice(0, 3).map(function (item, index) {
+ var status = text(first(item, ['status', 'state'])).toLowerCase();
+ return {
+ id:
+ text(first(item, ['id', 'task_id', 'taskId'])) ||
+ 'task-' + index,
+ title:
+ text(first(item, ['title', 'name'])) ||
+ DESIGN_TASKS[index].title,
+ description:
+ text(
+ first(item, ['description', 'desc', 'subtitle', 'hint'])
+ ) || DESIGN_TASKS[index].description,
+ reward: number(
+ first(item, [
+ 'reward_spins',
+ 'rewardSpins',
+ 'reward',
+ 'value',
+ ]),
+ 1
+ ),
+ completed:
+ first(item, [
+ 'completed',
+ 'is_completed',
+ 'isCompleted',
+ ]) === true ||
+ status === 'completed' ||
+ status === 'claimed',
+ action: first(item, ['action', 'action_url', 'actionUrl']),
+ raw: item,
+ };
+ });
+ }
+
+ function normalizeWalletBalance(payload) {
+ var data = envelope(payload);
+ var values = Array.isArray(data)
+ ? data
+ : data.balances || data.values || data.list || [];
+ if (!Array.isArray(values)) return 0;
+ var wallet = values.find(function (item) {
+ return (
+ text(
+ first(item, [
+ 'asset_type',
+ 'assetType',
+ 'wallet_type',
+ 'walletType',
+ 'type',
+ ])
+ ).toUpperCase() === 'COIN'
+ );
+ });
+ if (!wallet) return 0;
+ return number(
+ first(wallet, [
+ 'available_amount',
+ 'availableAmount',
+ 'balance',
+ 'amount',
+ ])
+ );
+ }
+
+ function createCommandID() {
+ var random = Math.random().toString(36).slice(2);
+ if (window.crypto && window.crypto.getRandomValues) {
+ var values = new Uint32Array(2);
+ window.crypto.getRandomValues(values);
+ random = values[0].toString(36) + values[1].toString(36);
+ }
+ return 'huwaa_cash_' + Date.now().toString(36) + '_' + random;
+ }
+
+ function setText(selector, value) {
+ var node = $(selector);
+ if (node) node.textContent = value;
+ }
+
+ function renderWallet() {
+ setText('#huwaaRatePoints', state.ratePoints);
+ setText('#huwaaPointBalance', formatNumber(state.pointBalance));
+ setText(
+ '#huwaaCashValue',
+ '≈$' +
+ number(state.cashValue, state.pointBalance / 10000).toFixed(1)
+ );
+ setText('#huwaaPointsLeft', formatNumber(state.withdrawalRemaining));
+ setText('#huwaaSpinCost', '*' + formatNumber(state.drawCost));
+ }
+
+ function renderPrizes() {
+ $all('.huwaa-prize').forEach(function (button, index) {
+ var prize = state.prizes[index] || DESIGN_PRIZES[index];
+ var image = button.querySelector('img');
+ var label = button.querySelector('span');
+ image.src = prize.image;
+ image.onerror = function () {
+ image.onerror = null;
+ image.src = DESIGN_PRIZES[index].image;
+ };
+ var prizeName = prize.raw
+ ? prize.name
+ : t('huwaa_invite.medium', 'Medium');
+ label.textContent = prizeName;
+ button.setAttribute(
+ 'aria-label',
+ prizeName +
+ (prize.points
+ ? ', ' + formatNumber(prize.points) + ' points'
+ : '')
+ );
+ });
+ }
+
+ function renderTasks() {
+ var list = $('#huwaaTaskList');
+ var template = $('#huwaaTaskTemplate');
+ if (!list || !template) return;
+ list.textContent = '';
+ state.tasks.slice(0, 3).forEach(function (task) {
+ var fragment = template.content.cloneNode(true);
+ var card = fragment.querySelector('.huwaa-task-card');
+ var status = fragment.querySelector('[data-task-status]');
+ fragment.querySelector('[data-task-title]').textContent =
+ task.titleKey ? t(task.titleKey, task.title) : task.title;
+ fragment.querySelector('[data-task-description]').textContent =
+ task.descriptionKey
+ ? t(task.descriptionKey, task.description)
+ : task.description;
+ fragment.querySelector('[data-task-reward]').textContent =
+ '+' + formatNumber(task.reward);
+ card.classList.toggle('is-pending', !task.completed);
+ if (task.completed) {
+ status.textContent = t('huwaa_invite.completed', 'COMPLETED');
+ } else {
+ var action = document.createElement('button');
+ action.className = status.className;
+ action.type = 'button';
+ action.textContent = t('huwaa_invite.go', 'GO');
+ action.addEventListener('click', function () {
+ openTask(task);
+ });
+ status.replaceWith(action);
+ }
+ list.appendChild(fragment);
+ });
+ }
+
+ function defaultEstimatedRewards() {
+ var values = [];
+ for (var index = 0; index < 12; index += 1) {
+ values.push({
+ id: 'estimated-' + index,
+ name: useMock ? 'DDDDD' : t('huwaa_invite.reward', 'Reward'),
+ image: ASSET_BASE + 'reward-calendar-check.webp',
+ points: 0,
+ });
+ }
+ return values;
+ }
+
+ function renderEstimatedRewards() {
+ var grid = $('#huwaaEstimatedGrid');
+ var template = $('#huwaaEstimatedTemplate');
+ if (!grid || !template) return;
+ var rewards = state.estimatedRewards.length
+ ? state.estimatedRewards
+ : defaultEstimatedRewards();
+ grid.textContent = '';
+ rewards.slice(0, 12).forEach(function (reward) {
+ var fragment = template.content.cloneNode(true);
+ var image = fragment.querySelector('img');
+ image.src =
+ reward.image || ASSET_BASE + 'reward-calendar-check.webp';
+ image.onerror = function () {
+ image.onerror = null;
+ image.src = ASSET_BASE + 'reward-calendar-check.webp';
+ };
+ fragment.querySelector('strong').textContent =
+ reward.name || t('huwaa_invite.reward', 'Reward');
+ grid.appendChild(fragment);
+ });
+ }
+
+ function renderAll() {
+ renderWallet();
+ renderPrizes();
+ renderTasks();
+ renderEstimatedRewards();
+ }
+
+ function setBusy(busy) {
+ state.busy = Boolean(busy);
+ $('#huwaaStage').classList.toggle('is-busy', state.busy);
+ $('#huwaaSpinButton').disabled = state.busy;
+ $('#huwaaStage').setAttribute('aria-busy', String(state.busy));
+ }
+
+ function setActivePrize(index) {
+ $all('.huwaa-prize').forEach(function (node, nodeIndex) {
+ node.classList.toggle('is-active', nodeIndex === index);
+ });
+ }
+
+ function wait(milliseconds) {
+ return new Promise(function (resolve) {
+ window.setTimeout(resolve, milliseconds);
+ });
+ }
+
+ function animateSpin(targetIndex) {
+ var total = state.prizes.length || DESIGN_PRIZES.length;
+ var loops = total * 2 + targetIndex + 1;
+ var chain = Promise.resolve();
+ for (var step = 0; step < loops; step += 1) {
+ (function (currentStep) {
+ chain = chain.then(function () {
+ setActivePrize(currentStep % total);
+ return wait(60 + Math.max(0, currentStep - loops + 6) * 25);
+ });
+ })(step);
+ }
+ return chain.then(function () {
+ setActivePrize(targetIndex);
+ });
+ }
+
+ function rewardTargetIndex(reward) {
+ if (!reward) return 0;
+ var id = text(reward.id).toLowerCase();
+ var name = text(reward.name).toLowerCase();
+ var points = number(reward.points);
+ var index = state.prizes.findIndex(function (prize) {
+ return id && text(prize.id).toLowerCase() === id;
+ });
+ if (index >= 0) return index;
+ index = state.prizes.findIndex(function (prize) {
+ return (
+ name &&
+ text(prize.name).toLowerCase() === name &&
+ (!points || number(prize.points) === points)
+ );
+ });
+ return index >= 0 ? index : 0;
+ }
+
+ function normalizeDrawResult(payload) {
+ var data = envelope(payload);
+ var rawRewards =
+ data.rewards ||
+ data.records ||
+ data.items ||
+ data.reward_items ||
+ [];
+ var firstReward = Array.isArray(rawRewards)
+ ? rawRewards[0]
+ : rawRewards;
+ var normalized = normalizeReward(
+ firstReward || data,
+ state.prizes[0],
+ 0
+ );
+ var balanceAfter = first(data, [
+ 'coin_balance_after',
+ 'coinBalanceAfter',
+ 'balance_after',
+ 'balanceAfter',
+ ]);
+ return {
+ reward: normalized,
+ balanceAfter:
+ balanceAfter === '' ? undefined : number(balanceAfter),
+ };
+ }
+
+ function mockDrawResult() {
+ var index = state.spinCount % state.prizes.length;
+ state.spinCount += 1;
+ return Promise.resolve({
+ reward: Object.assign({}, state.prizes[index], {
+ name: 'Medium Reward',
+ points: 500,
+ }),
+ // 视觉夹具保持 Figma 三个状态使用同一余额;真实环境只接受服务端返回的扣减后余额。
+ balanceAfter: undefined,
+ });
+ }
+
+ function requestDraw() {
+ if (useMock) return mockDrawResult();
+ var api = window.HyAppAPI && window.HyAppAPI.activityWheel;
+ var hasToken =
+ window.HyAppAPI &&
+ window.HyAppAPI.getAccessToken &&
+ window.HyAppAPI.getAccessToken();
+ if (!api || !api.draw || !hasToken) {
+ return Promise.reject(
+ new Error(
+ t('huwaa_invite.login_required', 'Please log in first')
+ )
+ );
+ }
+ // 抽奖金额和 command_id 必须一次性发给服务端;页面不在失败时本地补发奖励,避免网络错误造成视觉中奖但账本未入账。
+ return api
+ .draw(state.wheelID, {
+ command_id: createCommandID(),
+ amount: state.drawCost,
+ times: 1,
+ })
+ .then(normalizeDrawResult);
+ }
+
+ function showWin(reward) {
+ reward = reward || state.prizes[0];
+ var image = $('#huwaaWinImage');
+ image.src = reward.image || state.prizes[0].image;
+ image.onerror = function () {
+ image.onerror = null;
+ image.src = state.prizes[0].image;
+ };
+ setText(
+ '#huwaaWinReward',
+ reward.raw
+ ? reward.name
+ : t('huwaa_invite.medium_reward', 'Medium Reward')
+ );
+ setText(
+ '#huwaaWinPoints',
+ '(' +
+ formatNumber(reward.points || 0) +
+ ' ' +
+ t('huwaa_invite.points_label', 'Points') +
+ ')'
+ );
+ $('#huwaaWinModal').hidden = false;
+ syncModalLock();
+ $('#huwaaWinTitle').focus({ preventScroll: true });
+ }
+
+ function closeWin() {
+ $('#huwaaWinModal').hidden = true;
+ syncModalLock();
+ setActivePrize(-1);
+ $('#huwaaSpinButton').focus({ preventScroll: true });
+ }
+
+ function startSpin() {
+ if (state.busy) return;
+ setBusy(true);
+ requestDraw()
+ .then(function (result) {
+ var target = rewardTargetIndex(result.reward);
+ return animateSpin(target).then(function () {
+ if (result.balanceAfter !== undefined) {
+ state.pointBalance = result.balanceAfter;
+ state.cashValue = state.pointBalance / 10000;
+ renderWallet();
+ }
+ showWin(result.reward);
+ if (!useMock) loadEstimatedRewards();
+ });
+ })
+ .catch(function (error) {
+ setActivePrize(-1);
+ toast(
+ (error && error.message) ||
+ t('huwaa_invite.request_failed', 'Request failed')
+ );
+ })
+ .then(function () {
+ setBusy(false);
+ });
+ }
+
+ function openRewardDrawer() {
+ renderEstimatedRewards();
+ $('#huwaaRewardDrawer').hidden = false;
+ syncModalLock();
+ $('#huwaaRewardTitle').focus
+ ? $('#huwaaRewardTitle').focus({ preventScroll: true })
+ : undefined;
+ }
+
+ function closeRewardDrawer() {
+ $('#huwaaRewardDrawer').hidden = true;
+ syncModalLock();
+ $('#huwaaRewardEntry').focus({ preventScroll: true });
+ }
+
+ function syncModalLock() {
+ document.documentElement.classList.toggle(
+ 'huwaa-modal-open',
+ !$('#huwaaWinModal').hidden || !$('#huwaaRewardDrawer').hidden
+ );
+ }
+
+ function openTask(task) {
+ if (task.completed) return;
+ if (task.action) {
+ window.location.href = task.action;
+ return;
+ }
+ if (window.HyAppBridge && window.HyAppBridge.post) {
+ window.HyAppBridge.post('huwaaInviteTaskAction', {
+ task_id: task.id,
+ });
+ return;
+ }
+ toast(t('huwaa_invite.open_app_task', 'Open this task in Huwaa'));
+ }
+
+ function currentInviteLandingURL() {
+ return new URL('../invite-landing/', window.location.href);
+ }
+
+ function buildInviteLink() {
+ if (!state.inviteCode) return '';
+ var url = currentInviteLandingURL();
+ ['env', 'lang', 'app_code', 'appCode'].forEach(function (key) {
+ var value = text(params.get(key));
+ if (value) url.searchParams.set(key, value);
+ });
+ url.searchParams.set('app_code', 'huwaa');
+ url.searchParams.set('invite_code', state.inviteCode);
+ url.searchParams.set('inviteCode', state.inviteCode);
+ url.searchParams.set('code', state.inviteCode);
+ return url.toString();
+ }
+
+ function copyText(value) {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ return navigator.clipboard.writeText(value);
+ }
+ return new Promise(function (resolve, reject) {
+ var input = document.createElement('textarea');
+ input.value = value;
+ input.setAttribute('readonly', 'readonly');
+ input.style.position = 'fixed';
+ input.style.left = '-9999px';
+ document.body.appendChild(input);
+ input.select();
+ var copied = false;
+ try {
+ copied = document.execCommand('copy');
+ } catch (error) {
+ copied = false;
+ }
+ input.remove();
+ copied ? resolve() : reject(new Error('copy failed'));
+ });
+ }
+
+ function shareInvite() {
+ var link = state.inviteLink || buildInviteLink();
+ if (!link) {
+ toast(
+ t(
+ 'huwaa_invite.invite_unavailable',
+ 'Invitation link is not available yet'
+ )
+ );
+ return;
+ }
+ if (navigator.share) {
+ navigator
+ .share({
+ title: t('huwaa_invite.invite_now', 'Invite Friends Now'),
+ url: link,
+ })
+ .catch(function (error) {
+ if (error && error.name === 'AbortError') return;
+ copyInviteLink(link);
+ });
+ return;
+ }
+ copyInviteLink(link);
+ }
+
+ function copyInviteLink(link) {
+ copyText(link)
+ .then(function () {
+ toast(t('huwaa_invite.copied', 'Copied'));
+ })
+ .catch(function () {
+ toast(t('huwaa_invite.copy_failed', 'Copy failed'));
+ });
+ }
+
+ function withdraw() {
+ var walletURL = text(
+ params.get('wallet_url') || params.get('walletUrl')
+ );
+ if (walletURL) {
+ window.location.href = walletURL;
+ return;
+ }
+ if (window.HyAppBridge && window.HyAppBridge.post) {
+ window.HyAppBridge.post('huwaaCashWheelWithdraw', {
+ points: state.pointBalance,
+ });
+ return;
+ }
+ toast(
+ t('huwaa_invite.open_wallet', 'Open the Huwaa wallet to withdraw')
+ );
+ }
+
+ function goBack() {
+ if (window.HyAppBridge && window.HyAppBridge.back) {
+ window.HyAppBridge.back();
+ return;
+ }
+ if (window.history.length > 1) {
+ window.history.back();
+ }
+ }
+
+ function applyWheelConfig(payload) {
+ var data = envelope(payload);
+ var prizes = rewardItems(data);
+ var tasks = normalizeTasks(data);
+ var drawCost = number(
+ first(data, [
+ 'draw_price_coins',
+ 'drawPriceCoins',
+ 'draw_price',
+ 'drawPrice',
+ 'spin_cost',
+ 'spinCost',
+ ])
+ );
+ if (prizes.length) {
+ state.prizes = prizes.slice(0, 7);
+ while (state.prizes.length < 7) {
+ state.prizes.push(DESIGN_PRIZES[state.prizes.length]);
+ }
+ if (!state.estimatedRewards.length) {
+ state.estimatedRewards = prizes.slice(0, 12);
+ }
+ }
+ if (tasks.length) state.tasks = tasks;
+ if (drawCost > 0) state.drawCost = drawCost;
+ var threshold = number(
+ first(data, [
+ 'withdraw_threshold',
+ 'withdrawThreshold',
+ 'withdraw_points',
+ 'withdrawPoints',
+ ])
+ );
+ if (threshold > 0) {
+ state.ratePoints =
+ threshold >= 1000
+ ? Math.round(threshold / 1000) + 'K'
+ : formatNumber(threshold);
+ state.withdrawalRemaining = Math.max(
+ 0,
+ threshold - state.pointBalance
+ );
+ }
+ renderAll();
+ }
+
+ function loadWheelConfig() {
+ var api = window.HyAppAPI && window.HyAppAPI.activityWheel;
+ if (useMock || !api || !api.config) return Promise.resolve();
+ return api
+ .config(state.wheelID)
+ .then(applyWheelConfig)
+ .catch(function () {
+ // 配置缺失时保留 Figma 的独立本地视觉层,但抽奖仍走服务端且会失败提示,不在前端伪造奖励。
+ });
+ }
+
+ function loadWallet() {
+ var api = window.HyAppAPI && window.HyAppAPI.wallet;
+ var hasToken =
+ window.HyAppAPI &&
+ window.HyAppAPI.getAccessToken &&
+ window.HyAppAPI.getAccessToken();
+ if (useMock || !api || !api.balances || !hasToken) {
+ return Promise.resolve();
+ }
+ return api
+ .balances('COIN')
+ .then(function (payload) {
+ state.pointBalance = normalizeWalletBalance(payload);
+ state.cashValue = state.pointBalance / 10000;
+ renderWallet();
+ })
+ .catch(function () {
+ // 钱包失败不能阻塞页面结构;余额保持 0,实际抽奖仍由服务端余额校验兜底。
+ });
+ }
+
+ function normalizeHistoryRewards(payload) {
+ var data = envelope(payload);
+ var records = data.records || data.items || data.values || [];
+ if (!Array.isArray(records)) return [];
+ var rewards = [];
+ records.forEach(function (record) {
+ var values = record.rewards ||
+ record.items ||
+ record.reward_items || [record];
+ if (!Array.isArray(values)) values = [values];
+ values.forEach(function (value) {
+ rewards.push(
+ normalizeReward(
+ value,
+ DESIGN_PRIZES[rewards.length % DESIGN_PRIZES.length],
+ rewards.length
+ )
+ );
+ });
+ });
+ return rewards.slice(0, 12);
+ }
+
+ function loadEstimatedRewards() {
+ var api = window.HyAppAPI && window.HyAppAPI.activityWheel;
+ var hasToken =
+ window.HyAppAPI &&
+ window.HyAppAPI.getAccessToken &&
+ window.HyAppAPI.getAccessToken();
+ if (useMock || !api || !api.history || !hasToken) {
+ renderEstimatedRewards();
+ return Promise.resolve();
+ }
+ return api
+ .history(state.wheelID, { pageNo: 1, pageSize: 12 })
+ .then(function (payload) {
+ var rewards = normalizeHistoryRewards(payload);
+ if (rewards.length) state.estimatedRewards = rewards;
+ renderEstimatedRewards();
+ })
+ .catch(function () {
+ renderEstimatedRewards();
+ });
+ }
+
+ function applyInviteOverview(payload) {
+ var data = envelope(payload);
+ var invite = data.invite || (data.profile && data.profile.invite) || {};
+ state.inviteCode = text(
+ first(invite, [
+ 'my_invite_code',
+ 'myInviteCode',
+ 'invite_code',
+ 'inviteCode',
+ ]) ||
+ first(data, [
+ 'my_invite_code',
+ 'myInviteCode',
+ 'invite_code',
+ 'inviteCode',
+ ])
+ );
+ state.inviteLink = buildInviteLink();
+ }
+
+ function loadInviteOverview() {
+ if (useMock) {
+ state.inviteCode = 'HUWAA2026';
+ state.inviteLink = buildInviteLink();
+ return Promise.resolve();
+ }
+ var api = window.HyAppAPI;
+ var hasToken = api && api.getAccessToken && api.getAccessToken();
+ if (!api || !api.get || !hasToken) return Promise.resolve();
+ return api
+ .get('/api/v1/users/me/invite-overview')
+ .then(applyInviteOverview)
+ .catch(function () {
+ // 分享按钮在邀请数据缺失时保持不可分享状态,绝不拼接伪邀请码。
+ });
+ }
+
+ function bindEvents() {
+ $('#huwaaBackButton').addEventListener('click', goBack);
+ $('#huwaaWithdrawButton').addEventListener('click', withdraw);
+ $('#huwaaSpinButton').addEventListener('click', startSpin);
+ $('#huwaaInviteButton').addEventListener('click', shareInvite);
+ $('#huwaaRewardEntry').addEventListener('click', openRewardDrawer);
+ $('#huwaaAwesomeButton').addEventListener('click', closeWin);
+ $('[data-close-win]').addEventListener('click', closeWin);
+ $('[data-close-rewards]').addEventListener('click', closeRewardDrawer);
+ $all('.huwaa-prize').forEach(function (button) {
+ button.addEventListener('click', openRewardDrawer);
+ });
+ document.addEventListener('keydown', function (event) {
+ if (event.key !== 'Escape') return;
+ if (!$('#huwaaWinModal').hidden) closeWin();
+ if (!$('#huwaaRewardDrawer').hidden) closeRewardDrawer();
+ });
+ }
+
+ function init() {
+ bindEvents();
+ renderAll();
+ $('#huwaaPage').dataset.state = 'loading';
+ Promise.all([
+ loadWallet(),
+ loadWheelConfig(),
+ loadEstimatedRewards(),
+ loadInviteOverview(),
+ ]).then(function () {
+ $('#huwaaPage').dataset.state = 'ready';
+ });
+ if (window.HyAppBridge && window.HyAppBridge.ready) {
+ window.HyAppBridge.ready({
+ page: 'huwaa-invite-cash-wheel',
+ app_code: 'huwaa',
+ });
+ }
+ }
+
+ window.addEventListener('hyapp:i18n-ready', function () {
+ renderAll();
+ });
+
+ window.__HuwaaInvite = {
+ state: state,
+ openRewards: openRewardDrawer,
+ spin: startSpin,
+ };
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/activity/recharge-reward/fami.html b/activity/recharge-reward/fami.html
new file mode 100644
index 0000000..f99285e
--- /dev/null
+++ b/activity/recharge-reward/fami.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ Recharge Reward
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Days
+ Hours
+ Minutes
+ Seconds
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/recharge-reward/fami/assets/background-source-844-7068.png b/activity/recharge-reward/fami/assets/background-source-844-7068.png
new file mode 100644
index 0000000..fb664aa
Binary files /dev/null and b/activity/recharge-reward/fami/assets/background-source-844-7068.png differ
diff --git a/activity/recharge-reward/fami/assets/countdown-frame-822-2954.png b/activity/recharge-reward/fami/assets/countdown-frame-822-2954.png
new file mode 100644
index 0000000..a9ec701
Binary files /dev/null and b/activity/recharge-reward/fami/assets/countdown-frame-822-2954.png differ
diff --git a/activity/recharge-reward/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2 b/activity/recharge-reward/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2
new file mode 100644
index 0000000..8519f51
Binary files /dev/null and b/activity/recharge-reward/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2 differ
diff --git a/activity/recharge-reward/fami/assets/fonts/source-han-sans-sc-regular-latin.woff2 b/activity/recharge-reward/fami/assets/fonts/source-han-sans-sc-regular-latin.woff2
new file mode 100644
index 0000000..66eacd1
Binary files /dev/null and b/activity/recharge-reward/fami/assets/fonts/source-han-sans-sc-regular-latin.woff2 differ
diff --git a/activity/recharge-reward/fami/assets/mock-avatar-822-2960.png b/activity/recharge-reward/fami/assets/mock-avatar-822-2960.png
new file mode 100644
index 0000000..fe7706b
Binary files /dev/null and b/activity/recharge-reward/fami/assets/mock-avatar-822-2960.png differ
diff --git a/activity/recharge-reward/fami/assets/mock-reward-822-2971.png b/activity/recharge-reward/fami/assets/mock-reward-822-2971.png
new file mode 100644
index 0000000..cde45a5
Binary files /dev/null and b/activity/recharge-reward/fami/assets/mock-reward-822-2971.png differ
diff --git a/activity/recharge-reward/fami/assets/profile-frame-840-4737.png b/activity/recharge-reward/fami/assets/profile-frame-840-4737.png
new file mode 100644
index 0000000..088d592
Binary files /dev/null and b/activity/recharge-reward/fami/assets/profile-frame-840-4737.png differ
diff --git a/activity/recharge-reward/fami/assets/receive-skin-822-2990.png b/activity/recharge-reward/fami/assets/receive-skin-822-2990.png
new file mode 100644
index 0000000..2d8b099
Binary files /dev/null and b/activity/recharge-reward/fami/assets/receive-skin-822-2990.png differ
diff --git a/activity/recharge-reward/fami/assets/reward-card-822-2969.png b/activity/recharge-reward/fami/assets/reward-card-822-2969.png
new file mode 100644
index 0000000..2470005
Binary files /dev/null and b/activity/recharge-reward/fami/assets/reward-card-822-2969.png differ
diff --git a/activity/recharge-reward/fami/assets/reward-panel-822-2967.png b/activity/recharge-reward/fami/assets/reward-panel-822-2967.png
new file mode 100644
index 0000000..97f30b3
Binary files /dev/null and b/activity/recharge-reward/fami/assets/reward-panel-822-2967.png differ
diff --git a/activity/recharge-reward/fami/fami.css b/activity/recharge-reward/fami/fami.css
new file mode 100644
index 0000000..56542e0
--- /dev/null
+++ b/activity/recharge-reward/fami/fami.css
@@ -0,0 +1,437 @@
+@font-face {
+ font-family: 'Fami Source Han Sans SC';
+ src: url('./assets/fonts/source-han-sans-sc-regular-latin.woff2')
+ format('woff2');
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'Fami Source Han Sans SC';
+ src: url('./assets/fonts/source-han-sans-sc-medium-latin.woff2')
+ format('woff2');
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+:root {
+ --fami-design-width: 1080;
+ --fami-stage-scale: 0.3472222222;
+ --fami-stage-height: 3273px;
+ --fami-background-bottom-top: 2695px;
+}
+
+html,
+body {
+ margin: 0;
+ width: 100%;
+ min-height: 100%;
+ overflow-x: hidden;
+ background: #080116;
+ color: #f6e0b2;
+ font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
+}
+
+body {
+ display: flex;
+ justify-content: center;
+}
+
+img {
+ display: block;
+}
+
+.fami-recharge-app {
+ width: min(100vw, 430px);
+ min-height: calc(3273px * var(--fami-stage-scale));
+ overflow: hidden;
+ background: #080116;
+}
+
+.stage-wrap {
+ position: relative;
+ width: 100%;
+ height: calc(3273px * var(--fami-stage-scale));
+ overflow: hidden;
+}
+
+.stage {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 1080px;
+ height: var(--fami-stage-height);
+ overflow: hidden;
+ transform: scale(var(--fami-stage-scale));
+ transform-origin: top left;
+ background: #080116;
+}
+
+.activity-background,
+.reward-sections {
+ position: absolute;
+ inset: 0;
+}
+
+.activity-background {
+ z-index: 0;
+ pointer-events: none;
+}
+
+.background-segment {
+ position: absolute;
+ left: 0;
+ width: 1080px;
+ overflow: hidden;
+}
+
+.background-segment img {
+ position: absolute;
+ left: 0;
+ width: 1080px;
+ height: 1887px;
+ max-width: none;
+ pointer-events: none;
+}
+
+/* Figma 用同一背景节点的三段蒙版拼接长页;这里保留原始裁切坐标,而不是把根 Frame 当整页图片。 */
+.background-segment-mid {
+ top: 1311px;
+ height: 405px;
+ z-index: 0;
+}
+
+.background-segment-mid img {
+ top: -1168px;
+}
+
+.background-segment-top {
+ top: 0;
+ height: 1588px;
+ z-index: 1;
+}
+
+.background-segment-top img {
+ top: 0;
+}
+
+.background-segment-bottom {
+ top: var(--fami-background-bottom-top);
+ height: 578px;
+ z-index: 2;
+}
+
+.background-segment-bottom img {
+ top: -1333px;
+}
+
+.countdown,
+.profile-card,
+.reward-sections,
+.page-status {
+ position: absolute;
+ z-index: 3;
+}
+
+.countdown {
+ left: 90px;
+ top: 696px;
+ width: 900px;
+ height: 393px;
+}
+
+.countdown-frame {
+ position: absolute;
+ inset: 0;
+ width: 900px;
+ height: 393px;
+ pointer-events: none;
+}
+
+.count-label,
+.count-value {
+ position: absolute;
+ margin: 0;
+ transform: translateX(-50%);
+ text-align: center;
+ white-space: nowrap;
+}
+
+.count-label {
+ top: 280px;
+ width: 180px;
+ height: 24px;
+ color: #050300;
+ font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 24px;
+ text-transform: capitalize;
+}
+
+.count-value {
+ display: none;
+ top: 142px;
+ width: 150px;
+ height: 92px;
+ color: #f6e0b2;
+ font-size: 68px;
+ font-weight: 500;
+ line-height: 92px;
+ text-shadow: 0 3px 8px rgba(0, 0, 0, 0.72);
+}
+
+.countdown.has-values .count-value {
+ display: block;
+}
+
+.count-label-days,
+.count-value-days {
+ left: 145px;
+}
+
+.count-label-hours,
+.count-value-hours {
+ left: 349px;
+}
+
+.count-label-minutes,
+.count-value-minutes {
+ left: 553.5px;
+}
+
+.count-label-seconds,
+.count-value-seconds {
+ left: 753.5px;
+}
+
+.profile-card {
+ left: 90px;
+ top: 1089px;
+ width: 900px;
+ height: 679px;
+}
+
+.profile-frame {
+ position: absolute;
+ inset: 0;
+ width: 900px;
+ height: 679px;
+ pointer-events: none;
+}
+
+.profile-avatar {
+ position: absolute;
+ left: 309px;
+ top: 134px;
+ width: 282px;
+ height: 270px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.profile-name,
+.profile-id,
+.profile-recharge {
+ position: absolute;
+ left: 100px;
+ width: 700px;
+ margin: 0;
+ overflow: hidden;
+ color: #f6e0b2;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.profile-name {
+ top: 425px;
+ height: 36px;
+ font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
+ font-size: 36px;
+ font-weight: 400;
+ line-height: 36px;
+}
+
+.profile-id {
+ top: 476px;
+ height: 36px;
+ font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
+ font-size: 24px;
+ font-weight: 400;
+ line-height: 36px;
+}
+
+.profile-recharge {
+ top: 520px;
+ height: 40px;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 36px;
+}
+
+.reward-sections {
+ width: 1080px;
+ height: var(--fami-stage-height);
+}
+
+.reward-section {
+ position: absolute;
+ left: 0;
+ top: var(--section-top);
+ width: 1080px;
+ height: 720px;
+}
+
+.reward-panel {
+ position: absolute;
+ left: 90px;
+ top: 0;
+ width: 900px;
+ height: 689px;
+ pointer-events: none;
+}
+
+.reward-title {
+ position: absolute;
+ left: 529.5px;
+ top: 95px;
+ margin: 0;
+ transform: translateX(-50%);
+ color: #f6e0b2;
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 40px;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.reward-section.is-following .reward-title {
+ left: 559.5px;
+ top: 99px;
+}
+
+.reward-card {
+ position: absolute;
+ width: 229px;
+ height: 258px;
+ color: #f6e0b2;
+ text-align: center;
+}
+
+.reward-card-frame {
+ position: absolute;
+ inset: 0;
+ width: 229px;
+ height: 258px;
+ pointer-events: none;
+}
+
+.reward-media {
+ position: absolute;
+ left: 18px;
+ top: 19px;
+ width: 193px;
+ height: 193px;
+ object-fit: contain;
+}
+
+.reward-section.is-following .reward-media {
+ left: 18.5px;
+ top: 21px;
+}
+
+.reward-price {
+ position: absolute;
+ left: 0;
+ top: 202px;
+ width: 229px;
+ height: 32px;
+ overflow: hidden;
+ color: #f6e0b2;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 32px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.reward-action {
+ position: absolute;
+ left: 341px;
+ top: 591px;
+ width: 399px;
+ height: 129px;
+}
+
+.reward-section.is-following .reward-action {
+ top: 604px;
+}
+
+.reward-action-skin {
+ position: absolute;
+ inset: 0;
+ width: 399px;
+ height: 129px;
+ pointer-events: none;
+}
+
+.reward-action-label {
+ position: absolute;
+ left: 0;
+ top: 45px;
+ width: 399px;
+ height: 40px;
+ overflow: hidden;
+ color: #f6e0b2;
+ font-family:
+ 'SF Pro Display',
+ 'SF Pro Text',
+ -apple-system,
+ BlinkMacSystemFont,
+ Arial,
+ sans-serif;
+ font-size: 40px;
+ font-weight: 510;
+ font-variation-settings:
+ 'wght' 510,
+ 'wdth' 100;
+ line-height: 40px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.page-status {
+ left: 190px;
+ top: 1730px;
+ width: 700px;
+ margin: 0;
+ color: #f6e0b2;
+ font-size: 28px;
+ line-height: 40px;
+ text-align: center;
+}
+
+@media (min-width: 431px) {
+ html,
+ body {
+ background: #05000e;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/activity/recharge-reward/fami/fami.js b/activity/recharge-reward/fami/fami.js
new file mode 100644
index 0000000..3e7986c
--- /dev/null
+++ b/activity/recharge-reward/fami/fami.js
@@ -0,0 +1,495 @@
+(function () {
+ 'use strict';
+
+ var DESIGN_WIDTH = 1080;
+ var BASE_STAGE_HEIGHT = 3273;
+ var SECTION_STEP = 720;
+ var MAX_VIEWPORT_WIDTH = 430;
+ var STATUS_PATH = '/api/v1/activities/cumulative-recharge-reward';
+
+ var appViewport = document.getElementById('appViewport');
+ var stageWrap = document.getElementById('stageWrap');
+ var stage = document.getElementById('stage');
+ var countdown = document.getElementById('countdown');
+ var rewardSections = document.getElementById('rewardSections');
+ var profileName = document.getElementById('profileName');
+ var profileID = document.getElementById('profileID');
+ var profileRecharge = document.getElementById('profileRecharge');
+ var profileAvatar = document.getElementById('profileAvatar');
+ var pageStatus = document.getElementById('pageStatus');
+ var countNodes = {
+ days: document.querySelector('[data-count="days"]'),
+ hours: document.querySelector('[data-count="hours"]'),
+ minutes: document.querySelector('[data-count="minutes"]'),
+ seconds: document.querySelector('[data-count="seconds"]'),
+ };
+
+ var currentData = null;
+ var designHeight = BASE_STAGE_HEIGHT;
+ var countdownEndAt = 0;
+ var timeOffsetMS = 0;
+ var countdownTimer = 0;
+
+ function queryParams() {
+ return new URLSearchParams(window.location.search || '');
+ }
+
+ function shouldUseMock(params) {
+ var value = String(params.get('mock') || '').toLowerCase();
+ // 设计数据只允许显式 mock=1/true;生产接口失败时不得回退到看似可用的虚假奖励。
+ return value === '1' || value === 'true';
+ }
+
+ function t(key, fallback) {
+ return window.HyAppI18n && typeof window.HyAppI18n.t === 'function'
+ ? window.HyAppI18n.t(key, fallback)
+ : fallback || key;
+ }
+
+ function escapeHTML(value) {
+ return String(value == null ? '' : value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ function firstValue(source, keys, fallback) {
+ source = source && typeof source === 'object' ? source : {};
+ for (var index = 0; index < keys.length; index += 1) {
+ var value = source[keys[index]];
+ if (value !== undefined && value !== null && value !== '')
+ return value;
+ }
+ return fallback;
+ }
+
+ function numberValue(source, keys, fallback) {
+ var value = Number(firstValue(source, keys, fallback));
+ return Number.isFinite(value) ? value : Number(fallback || 0);
+ }
+
+ function wholeUSD(minor) {
+ var value = Math.max(0, Number(minor || 0)) / 100;
+ return value % 1 === 0
+ ? String(value)
+ : value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
+ }
+
+ function tierAmount(minor) {
+ return '$' + wholeUSD(minor);
+ }
+
+ function twoDigits(value) {
+ return String(Math.max(0, Math.floor(value))).padStart(2, '0');
+ }
+
+ function updateScale() {
+ var viewportWidth = Math.min(
+ window.innerWidth || 375,
+ MAX_VIEWPORT_WIDTH
+ );
+ var scale = viewportWidth / DESIGN_WIDTH;
+ var renderedHeight = designHeight * scale;
+ document.documentElement.style.setProperty(
+ '--fami-stage-scale',
+ String(scale)
+ );
+ document.documentElement.style.setProperty(
+ '--fami-stage-height',
+ designHeight + 'px'
+ );
+ document.documentElement.style.setProperty(
+ '--fami-background-bottom-top',
+ designHeight - 578 + 'px'
+ );
+ appViewport.style.minHeight = renderedHeight + 'px';
+ stageWrap.style.height = renderedHeight + 'px';
+ stage.style.height = designHeight + 'px';
+ }
+
+ function mockData(params) {
+ var rechargeValue = Number(params.get('recharge') || 5998);
+ var explicitEnd = Date.parse(
+ params.get('end_at') || params.get('endAt') || ''
+ );
+ var rewardItem = {
+ item_type: 'resource',
+ resource: {
+ name: 'Figma reward fixture',
+ preview_url: './fami/assets/mock-reward-822-2971.png',
+ },
+ };
+ return {
+ enabled: true,
+ user: {
+ username: params.get('name') || 'nanfangjian',
+ display_user_id:
+ params.get('uid') || params.get('id') || '12345678',
+ avatar:
+ params.get('avatar') ||
+ './fami/assets/mock-avatar-822-2960.png',
+ },
+ period: {
+ // Figma 节点没有倒计时数字;只有显式 end_at 才展示动态数字,避免凭空猜一个验收值。
+ end_ms: Number.isFinite(explicitEnd) ? explicitEnd : 0,
+ },
+ progress: {
+ total_usd_minor: Math.max(0, Math.round(rechargeValue * 100)),
+ },
+ tiers: [
+ {
+ tier_id: 1,
+ threshold_usd_minor: 10000,
+ reached: true,
+ resource_group: { items: [rewardItem, rewardItem] },
+ },
+ {
+ tier_id: 2,
+ threshold_usd_minor: 10000,
+ reached: true,
+ resource_group: {
+ items: [rewardItem, rewardItem, rewardItem],
+ },
+ },
+ ],
+ grants: [],
+ server_time_ms: Date.now(),
+ };
+ }
+
+ function resourcePreview(item) {
+ var resource = (item && item.resource) || {};
+ // preview_url 是 gateway 明确下发给静态 H5 的封面;asset_url 可能是动画,仅作为旧数据兜底。
+ return String(
+ firstValue(
+ resource,
+ [
+ 'preview_url',
+ 'previewUrl',
+ 'asset_url',
+ 'assetUrl',
+ 'animation_url',
+ 'animationUrl',
+ ],
+ ''
+ ) || ''
+ );
+ }
+
+ function normalizeData(payload) {
+ payload = payload && payload.status ? payload.status : payload || {};
+ var config = payload.config || {};
+ var rawTiers = Array.isArray(payload.tiers)
+ ? payload.tiers
+ : Array.isArray(config.tiers)
+ ? config.tiers
+ : [];
+ var grants = Array.isArray(payload.grants) ? payload.grants : [];
+ var grantByTier = {};
+ grants.forEach(function (grant) {
+ var tierID = String(firstValue(grant, ['tier_id', 'tierId'], ''));
+ if (tierID) grantByTier[tierID] = grant;
+ });
+
+ var tiers = rawTiers
+ .slice()
+ .sort(function (a, b) {
+ return (
+ numberValue(a, ['sort_order', 'sortOrder'], 0) -
+ numberValue(b, ['sort_order', 'sortOrder'], 0)
+ );
+ })
+ .map(function (tier) {
+ var group = tier.resource_group || tier.resourceGroup || {};
+ var items = Array.isArray(group.items) ? group.items : [];
+ var tierID = String(
+ firstValue(tier, ['tier_id', 'tierId'], '')
+ );
+ var grant = tier.grant || grantByTier[tierID] || null;
+ return {
+ tierID: tierID,
+ thresholdUSDMinor: numberValue(
+ tier,
+ ['threshold_usd_minor', 'thresholdUsdMinor'],
+ 0
+ ),
+ reached: Boolean(firstValue(tier, ['reached'], false)),
+ grant: grant,
+ cards: items.slice(0, 3).map(function (item) {
+ return {
+ name: firstValue(
+ (item && item.resource) || {},
+ ['name'],
+ ''
+ ),
+ preview: resourcePreview(item),
+ };
+ }),
+ };
+ });
+
+ return {
+ enabled:
+ firstValue(
+ payload,
+ ['enabled'],
+ firstValue(config, ['enabled'], true)
+ ) !== false,
+ user: payload.user || {},
+ period: payload.period || {},
+ progress: payload.progress || {},
+ serverTimeMS: numberValue(
+ payload,
+ ['server_time_ms', 'serverTimeMs'],
+ Date.now()
+ ),
+ tiers: tiers,
+ };
+ }
+
+ function grantWasIssued(grant) {
+ if (!grant) return false;
+ var status = String(firstValue(grant, ['status'], '')).toLowerCase();
+ return (
+ ['granted', 'success', 'succeeded', 'completed'].indexOf(status) >=
+ 0
+ );
+ }
+
+ function cardPositions(count) {
+ if (count <= 1) return [425.5];
+ if (count === 2) return [201, 650];
+ return [160.5, 429.5, 698.5];
+ }
+
+ function renderCard(card, section, cardIndex, left) {
+ var media = card.preview
+ ? '
'
+ : '';
+ return [
+ '',
+ '
',
+ media,
+ '',
+ escapeHTML(tierAmount(section.thresholdUSDMinor)),
+ '',
+ '',
+ ].join('');
+ }
+
+ function renderSections(tiers) {
+ designHeight = Math.max(
+ BASE_STAGE_HEIGHT,
+ BASE_STAGE_HEIGHT + Math.max(0, tiers.length - 2) * SECTION_STEP
+ );
+ rewardSections.innerHTML = tiers
+ .map(function (tier, sectionIndex) {
+ var section = {
+ thresholdUSDMinor: tier.thresholdUSDMinor,
+ cardTop: sectionIndex === 0 ? 293 : 278,
+ };
+ var positions = cardPositions(tier.cards.length);
+ var actionKey = grantWasIssued(tier.grant)
+ ? 'rechargeReward.claimed'
+ : 'rechargeReward.receive';
+ var actionFallback = grantWasIssued(tier.grant)
+ ? 'Claimed'
+ : 'Receive';
+ return [
+ '',
+ '
',
+ '',
+ escapeHTML(
+ t(
+ 'rechargeReward.rechargeTitle',
+ 'Recharge {amount}'
+ ).replace(
+ '{amount}',
+ tierAmount(tier.thresholdUSDMinor)
+ )
+ ),
+ '
',
+ tier.cards
+ .map(function (card, cardIndex) {
+ return renderCard(
+ card,
+ section,
+ cardIndex,
+ positions[cardIndex]
+ );
+ })
+ .join(''),
+ // 累充奖励由 wallet 充值事件自动发放,gateway 没有 claim RPC;设计中的 Receive 仅显示服务端状态,禁止伪造领取请求。
+ '',
+ '

',
+ '
',
+ escapeHTML(t(actionKey, actionFallback)),
+ '',
+ '
',
+ '',
+ ].join('');
+ })
+ .join('');
+ updateScale();
+ }
+
+ function renderProfile(data) {
+ var user = data.user || {};
+ var displayID = firstValue(
+ user,
+ ['display_user_id', 'displayUserId', 'user_id', 'userId'],
+ ''
+ );
+ var name = firstValue(user, ['username', 'name'], '');
+ var avatar = String(
+ firstValue(user, ['avatar', 'avatar_url', 'avatarUrl'], '') || ''
+ );
+ var totalMinor = numberValue(
+ data.progress || {},
+ ['total_usd_minor', 'totalUsdMinor'],
+ 0
+ );
+
+ profileName.textContent = name;
+ profileID.textContent = displayID
+ ? t('rechargeReward.idPrefix', 'ID:') + ' ' + displayID
+ : '';
+ profileRecharge.textContent =
+ t('rechargeReward.accumulatedRecharge', 'Accumulated Recharge:') +
+ wholeUSD(totalMinor);
+ if (avatar) {
+ profileAvatar.src = avatar;
+ profileAvatar.hidden = false;
+ } else {
+ profileAvatar.removeAttribute('src');
+ profileAvatar.hidden = true;
+ }
+ }
+
+ function updateCountdown() {
+ if (!countdownEndAt) {
+ countdown.classList.remove('has-values');
+ Object.keys(countNodes).forEach(function (key) {
+ countNodes[key].textContent = '';
+ });
+ return;
+ }
+ var remaining = Math.max(
+ 0,
+ countdownEndAt - (Date.now() - timeOffsetMS)
+ );
+ var totalSeconds = Math.floor(remaining / 1000);
+ countNodes.days.textContent = twoDigits(totalSeconds / 86400);
+ countNodes.hours.textContent = twoDigits((totalSeconds % 86400) / 3600);
+ countNodes.minutes.textContent = twoDigits((totalSeconds % 3600) / 60);
+ countNodes.seconds.textContent = twoDigits(totalSeconds % 60);
+ countdown.classList.add('has-values');
+ }
+
+ function render(data) {
+ currentData = normalizeData(data);
+ renderProfile(currentData);
+ renderSections(currentData.tiers);
+ timeOffsetMS = Date.now() - currentData.serverTimeMS;
+ countdownEndAt = numberValue(
+ currentData.period,
+ ['end_ms', 'endMs'],
+ 0
+ );
+ updateCountdown();
+ if (countdownTimer) window.clearInterval(countdownTimer);
+ if (countdownEndAt)
+ countdownTimer = window.setInterval(updateCountdown, 1000);
+ pageStatus.hidden = true;
+ pageStatus.textContent = '';
+ stage.setAttribute('aria-busy', 'false');
+ }
+
+ function loadStatus(params) {
+ if (shouldUseMock(params)) return Promise.resolve(mockData(params));
+ if (!window.HyAppAPI || typeof window.HyAppAPI.get !== 'function') {
+ return Promise.reject(new Error('api_unavailable'));
+ }
+ return window.HyAppAPI.get(STATUS_PATH);
+ }
+
+ function showLoadError(error) {
+ console.warn('[recharge-reward/fami] status load failed', error);
+ currentData = null;
+ rewardSections.innerHTML = '';
+ profileName.textContent = '';
+ profileID.textContent = '';
+ profileRecharge.textContent = '';
+ profileAvatar.hidden = true;
+ pageStatus.textContent = t('rechargeReward.loadFailed', 'Load failed');
+ pageStatus.hidden = false;
+ stage.setAttribute('aria-busy', 'false');
+ }
+
+ function init() {
+ var params = queryParams();
+ document.title = t('rechargeReward.pageTitle', 'Recharge Reward');
+ updateScale();
+ loadStatus(params).then(render).catch(showLoadError);
+
+ window.addEventListener('resize', updateScale);
+ window.addEventListener('hyapp:i18n-ready', function () {
+ document.title = t('rechargeReward.pageTitle', 'Recharge Reward');
+ if (currentData) {
+ // 语言包只影响 DOM 文案;复用已归一化的业务状态,不能再次按原始 API 结构解析而丢失奖励卡。
+ renderProfile(currentData);
+ renderSections(currentData.tiers);
+ updateCountdown();
+ } else if (!pageStatus.hidden)
+ pageStatus.textContent = t(
+ 'rechargeReward.loadFailed',
+ 'Load failed'
+ );
+ });
+
+ // 远程头像或奖励封面失效时只隐藏业务媒体,装饰框和真实金额状态仍保留,避免回退到伪造素材。
+ stage.addEventListener(
+ 'error',
+ function (event) {
+ if (
+ event.target === profileAvatar ||
+ (event.target &&
+ event.target.classList.contains('reward-media'))
+ ) {
+ event.target.hidden = true;
+ }
+ },
+ true
+ );
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init, { once: true });
+ } else {
+ init();
+ }
+})();
diff --git a/activity/recharge-reward/fami/manifest.json b/activity/recharge-reward/fami/manifest.json
new file mode 100644
index 0000000..ae74248
--- /dev/null
+++ b/activity/recharge-reward/fami/manifest.json
@@ -0,0 +1,103 @@
+{
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "page": "Fami运营",
+ "rootNode": "822:2928",
+ "rootSize": {
+ "width": 1080,
+ "height": 3273
+ }
+ },
+ "rules": {
+ "rootFrameUsedAsAsset": false,
+ "dynamicTextRenderedByDOM": true,
+ "productionBusinessMediaFromAPI": true,
+ "receiveControlMode": "read-only grant status; backend grants automatically and exposes no claim RPC"
+ },
+ "assets": [
+ {
+ "path": "assets/background-source-844-7068.png",
+ "sourceNode": "844:7068",
+ "purpose": "single-node decorative background source, clipped into the three Figma mask ranges by CSS",
+ "width": 1080,
+ "height": 1887,
+ "sha256": "1ee73b4ba3a47cd8cafbc6d42bd7350cdcfc25237f022991d7d190d7d8b22212",
+ "mockOnly": false
+ },
+ {
+ "path": "assets/countdown-frame-822-2954.png",
+ "sourceNode": "822:2954",
+ "purpose": "countdown ornament and empty slots; labels and values remain DOM",
+ "width": 900,
+ "height": 393,
+ "sha256": "fe4cccc3747f37d9c2cf127e704d68a1e453df61c5f6241b187d29f0291d525a",
+ "mockOnly": false
+ },
+ {
+ "path": "assets/profile-frame-840-4737.png",
+ "sourceNode": "840:4737",
+ "purpose": "profile card ornament; avatar, name, id and recharge amount remain independent DOM/data",
+ "width": 900,
+ "height": 679,
+ "sha256": "15946e593bdd4341e78fadf1a7b63202d54b38e650aabba6ed2397428e3f6377",
+ "mockOnly": false
+ },
+ {
+ "path": "assets/reward-panel-822-2967.png",
+ "sourceNode": "822:2967",
+ "purpose": "reward tier panel ornament; tier title, cards and state remain DOM",
+ "width": 900,
+ "height": 689,
+ "sha256": "2464ef72797d6f30730a2d960bbdf9da0a89cab86144937c23614dfdcbdf508f",
+ "mockOnly": false
+ },
+ {
+ "path": "assets/reward-card-822-2969.png",
+ "sourceNode": "822:2969",
+ "purpose": "single reward-card skin; resource preview and amount remain independent",
+ "width": 229,
+ "height": 258,
+ "sha256": "a7029c276f2b6bc05a89c26c89db1f654f587d8be90347b9f29cd36a41381c69",
+ "mockOnly": false
+ },
+ {
+ "path": "assets/receive-skin-822-2990.png",
+ "sourceNode": "822:2990",
+ "purpose": "status skin only; Receive or Claimed is DOM text and is not a fake claim control",
+ "width": 399,
+ "height": 129,
+ "sha256": "a32fbee534c2a0ae98f16bc3624b6e575b78493bebdc72fd026d2172415f8126",
+ "mockOnly": false
+ },
+ {
+ "path": "assets/mock-avatar-822-2960.png",
+ "sourceNode": "822:2960",
+ "purpose": "explicit mock=1 Figma fixture only; production avatar comes from status.user.avatar",
+ "width": 282,
+ "height": 270,
+ "sha256": "97c96761e348811f982cd8216b8e8f312f908337a692cccaca4212d189df2b09",
+ "mockOnly": true
+ },
+ {
+ "path": "assets/mock-reward-822-2971.png",
+ "sourceNode": "822:2971",
+ "purpose": "explicit mock=1 Figma fixture only; production previews come from resource.preview_url",
+ "width": 193,
+ "height": 193,
+ "sha256": "32e56831590c49ab34b1f06c5aaaeffadd370564beab4742fabf3a06323370b3",
+ "mockOnly": true
+ }
+ ],
+ "fonts": [
+ {
+ "path": "assets/fonts/source-han-sans-sc-regular-latin.woff2",
+ "source": "https://github.com/adobe-fonts/source-han-sans/tree/release/OTF/SimplifiedChinese/SourceHanSansSC-Regular.otf",
+ "sha256": "5a1880101a54dfbf9a4fce716eae6f4e9e84e3d1fd7b465613eee00dc22ee03c"
+ },
+ {
+ "path": "assets/fonts/source-han-sans-sc-medium-latin.woff2",
+ "source": "activity/cp-space/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2",
+ "sha256": "0c5b80890935bdf1ab542338c7f5ebe5c4fc678c76873c843a9c3be1c6b23e57"
+ }
+ ]
+}
diff --git a/activity/room-reward/fami.html b/activity/room-reward/fami.html
new file mode 100644
index 0000000..8c835f9
--- /dev/null
+++ b/activity/room-reward/fami.html
@@ -0,0 +1,178 @@
+
+
+
+
+
+ Room Reward Event
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+

+
—
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/room-reward/fami/assets/layers/avatar-mock.webp b/activity/room-reward/fami/assets/layers/avatar-mock.webp
new file mode 100644
index 0000000..97691e8
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/avatar-mock.webp differ
diff --git a/activity/room-reward/fami/assets/layers/coin-target.webp b/activity/room-reward/fami/assets/layers/coin-target.webp
new file mode 100644
index 0000000..bbd49af
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/coin-target.webp differ
diff --git a/activity/room-reward/fami/assets/layers/coin-tier.webp b/activity/room-reward/fami/assets/layers/coin-tier.webp
new file mode 100644
index 0000000..3da61ee
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/coin-tier.webp differ
diff --git a/activity/room-reward/fami/assets/layers/frame-purple-gold.webp b/activity/room-reward/fami/assets/layers/frame-purple-gold.webp
new file mode 100644
index 0000000..2471a88
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/frame-purple-gold.webp differ
diff --git a/activity/room-reward/fami/assets/layers/hero-fade-mask.svg b/activity/room-reward/fami/assets/layers/hero-fade-mask.svg
new file mode 100644
index 0000000..05a1348
--- /dev/null
+++ b/activity/room-reward/fami/assets/layers/hero-fade-mask.svg
@@ -0,0 +1,9 @@
+
diff --git a/activity/room-reward/fami/assets/layers/hero-room-reward.webp b/activity/room-reward/fami/assets/layers/hero-room-reward.webp
new file mode 100644
index 0000000..b5ba82c
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/hero-room-reward.webp differ
diff --git a/activity/room-reward/fami/assets/layers/pill-disabled.webp b/activity/room-reward/fami/assets/layers/pill-disabled.webp
new file mode 100644
index 0000000..b507561
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/pill-disabled.webp differ
diff --git a/activity/room-reward/fami/assets/layers/table-border-ornament.webp b/activity/room-reward/fami/assets/layers/table-border-ornament.webp
new file mode 100644
index 0000000..f5d7fc9
Binary files /dev/null and b/activity/room-reward/fami/assets/layers/table-border-ornament.webp differ
diff --git a/activity/room-reward/fami/assets/manifest.json b/activity/room-reward/fami/assets/manifest.json
new file mode 100644
index 0000000..e0cd6e3
--- /dev/null
+++ b/activity/room-reward/fami/assets/manifest.json
@@ -0,0 +1,112 @@
+{
+ "schemaVersion": 1,
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "frameNode": "822:2775",
+ "frameName": "room reward event",
+ "designSize": [1080, 1985]
+ },
+ "policy": {
+ "forbiddenNodes": ["822:2775", "822:2783"],
+ "forbiddenRuntimeAssets": [".audit/figma-default.png"],
+ "rule": "Only single visual layers are assets. Text, table rows, state, controls, and dynamic media are DOM/CSS/API driven."
+ },
+ "assets": [
+ {
+ "file": "hero-room-reward.webp",
+ "nodes": ["822:2778"],
+ "role": "hero illustration",
+ "kind": "single-raster-illustration",
+ "design": [1080, 810],
+ "note": "Single Figma image-fill artwork; dynamic page content is not embedded.",
+ "sha256": "e26e31a04e42a1562aecd7671c8c3d59f54e3b462b1b16d73dcc075f4ce89653",
+ "sourceSha256": "f8236a4fe58aadc228993f415146eb70832e7edfa9291e31736731d7cf8ab8ae",
+ "pixelSize": [1080, 810],
+ "sourceFile": "hero-source.bin"
+ },
+ {
+ "file": "hero-fade-mask.svg",
+ "nodes": ["822:2777"],
+ "role": "hero bottom fade mask",
+ "kind": "single-mask",
+ "design": [1080, 810],
+ "note": "Mask for the hero illustration only.",
+ "sha256": "a2f81f0beaabab653ba390bfa0d8f0fa3bfcc7ad01a0c3e9744d727163c069b6",
+ "sourceSha256": "a2f81f0beaabab653ba390bfa0d8f0fa3bfcc7ad01a0c3e9744d727163c069b6",
+ "pixelSize": [1080, 810],
+ "sourceFile": "hero-mask.bin"
+ },
+ {
+ "file": "frame-purple-gold.webp",
+ "nodes": ["822:2779", "822:2911"],
+ "role": "target and active value ornamental frame",
+ "kind": "single-ornamental-frame",
+ "design": [900, 289],
+ "note": "Reused at 215x69 for the value pill; all numbers remain DOM text.",
+ "sha256": "3e4dbc62c88914cf7dd9b06daf67afd8a9fcbb7af12cd40f4fbaacd88eac53bb",
+ "sourceSha256": "98c9dfbea98bca32e7cf97cf4f6e62185f472b7ca7bcfa4ff494aab7695b1994",
+ "pixelSize": [900, 289],
+ "sourceFile": "purple-frame.bin"
+ },
+ {
+ "file": "table-border-ornament.webp",
+ "nodes": ["822:2789", "822:2795", "822:2830"],
+ "role": "table top and side border ornament",
+ "kind": "single-ornamental-border-source",
+ "design": [1008, 1003],
+ "note": "CSS clips one source into top/left/right decorative rails; rows and labels remain DOM.",
+ "sha256": "912ab4da6b606cbe75c618e37123c30538e87302d1d9e137690f4c0ef57fdf17",
+ "sourceSha256": "3d854ee220f85b25a7525f2e4255e9f741f7aaf100853f5661b6dce58128572c",
+ "pixelSize": [1008, 1003],
+ "sourceFile": "border-source.bin"
+ },
+ {
+ "file": "coin-target.webp",
+ "nodes": ["822:2782"],
+ "role": "target currency icon",
+ "kind": "single-icon",
+ "design": [44, 44],
+ "note": "Decorative currency icon beside a DOM value.",
+ "sha256": "3c78529eaf1c85ccb56ceb5af17ed7b1ba95f61229d7d085e10ed11e2941a0b9",
+ "sourceSha256": "28f2737ecf85a13858b02b45b447c132ea2bee6cc32d6eceec95f17c85ac0d0c",
+ "pixelSize": [44, 44],
+ "sourceFile": "coin-large.bin"
+ },
+ {
+ "file": "coin-tier.webp",
+ "nodes": ["822:2869"],
+ "role": "tier reward currency icon",
+ "kind": "single-icon",
+ "design": [38, 38],
+ "note": "Repeated icon only; reward values remain DOM text.",
+ "sha256": "796d13d982fd4d96f30312a61eddc65a683d6d32a508db766df00298f3364009",
+ "sourceSha256": "f0ac90be6a9dc4fb471d6b6fe9a03a0e465b8ddcb11b932f6e7cd10b5d1a0251",
+ "pixelSize": [38, 38],
+ "sourceFile": "coin-small.bin"
+ },
+ {
+ "file": "pill-disabled.webp",
+ "nodes": ["822:2913"],
+ "role": "disabled reward value ornamental frame",
+ "kind": "single-ornamental-frame",
+ "design": [215, 69],
+ "note": "Display frame only; not a claim control.",
+ "sha256": "a6caf2d3311b4d99d32fbd9ff63eb6debe1bb9d208ad45ec9551cf69190b221e",
+ "sourceSha256": "d93927d705a527ba7528d1399fed3eac3afc649530c3fda024f8b6e994e7bf46",
+ "pixelSize": [215, 69],
+ "sourceFile": "button-disabled.bin"
+ },
+ {
+ "file": "avatar-mock.webp",
+ "nodes": ["822:2904"],
+ "role": "Figma QA mock avatar",
+ "kind": "single-dynamic-media-fixture",
+ "design": [119, 119],
+ "note": "Used only when ?mock=1; production uses API user.avatar/room.cover_url.",
+ "sha256": "6297d792db71672c9eb39103ebc1cad51257f6243ce115bd2a11bb51ad57dc7f",
+ "sourceSha256": "42f8c005b3c77796b55dac4cfe5096fbe2773d46c9d78d1114b59a41c45b5a68",
+ "pixelSize": [384, 512],
+ "sourceFile": "avatar-source.bin"
+ }
+ ]
+}
diff --git a/activity/room-reward/fami/fami.css b/activity/room-reward/fami/fami.css
new file mode 100644
index 0000000..e5128cc
--- /dev/null
+++ b/activity/room-reward/fami/fami.css
@@ -0,0 +1,468 @@
+@font-face {
+ font-family: 'RR Source Han Sans';
+ src: url('../../cp-space/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2')
+ format('woff2');
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+:root {
+ --rr-scale: 0.3472222222;
+ --rr-page-width: 375px;
+ --rr-stage-height: 1985px;
+ --rr-purple-bg: #160029;
+ --rr-row-dark: #0e0119;
+ --rr-footer-bg: #320b55;
+}
+
+html,
+body {
+ margin: 0;
+ width: 100%;
+ min-height: 100%;
+ overflow-x: hidden;
+ background: var(--rr-purple-bg, var(--hy-theme-bg));
+ color: #fff;
+ font-family: 'RR Source Han Sans', Arial, 'Helvetica Neue', sans-serif;
+}
+
+body {
+ min-height: 100vh;
+}
+
+img {
+ display: block;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.rr-app {
+ position: relative;
+ width: var(--rr-page-width);
+ min-height: 100vh;
+ margin: 0 auto;
+ overflow: visible;
+ background: var(--rr-purple-bg);
+}
+
+.rr-stage-shell {
+ position: relative;
+ width: var(--rr-page-width);
+ height: 689.236px;
+ overflow: hidden;
+ background: var(--rr-purple-bg);
+}
+
+.rr-stage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: var(--rr-stage-height);
+ overflow: hidden;
+ transform: scale(var(--rr-scale));
+ transform-origin: top left;
+ background: var(--rr-purple-bg);
+}
+
+.rr-hero {
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 810px;
+ pointer-events: none;
+ -webkit-mask-image: url('./assets/layers/hero-fade-mask.svg');
+ -webkit-mask-position: 0 0;
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-size: 1080px 810px;
+ mask-image: url('./assets/layers/hero-fade-mask.svg');
+ mask-position: 0 0;
+ mask-repeat: no-repeat;
+ mask-size: 1080px 810px;
+}
+
+.rr-hero img {
+ width: 1080px;
+ height: 810px;
+ object-fit: fill;
+}
+
+.rr-target {
+ position: absolute;
+ z-index: 3;
+ top: 665px;
+ left: 90px;
+ width: 900px;
+ height: 289px;
+}
+
+.rr-target-skin {
+ position: absolute;
+ inset: 0;
+ width: 900px;
+ height: 289px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.rr-target-value {
+ position: absolute;
+ top: 105px;
+ left: 330px;
+ display: flex;
+ align-items: center;
+ width: 241px;
+ height: 80px;
+ gap: 14px;
+ color: #ffe6b9;
+ white-space: nowrap;
+}
+
+.rr-target-value img {
+ flex: 0 0 auto;
+ width: 44px;
+ height: 44px;
+ object-fit: contain;
+ position: relative;
+ top: 8px;
+}
+
+.rr-target-value strong {
+ display: block;
+ width: 183px;
+ min-width: 0;
+ overflow: hidden;
+ color: #ffe6b9;
+ font-size: 80px;
+ font-weight: 500;
+ line-height: 80px;
+ letter-spacing: 0;
+ text-overflow: ellipsis;
+}
+
+.rr-table {
+ position: absolute;
+ z-index: 2;
+ top: 1005px;
+ left: 36px;
+ width: 1008px;
+ height: 980px;
+ overflow: hidden;
+}
+
+.rr-table-top-ornament {
+ position: absolute;
+ z-index: 4;
+ top: 0;
+ left: 0;
+ width: 1008px;
+ height: 15px;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.rr-table-top-ornament img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1008px;
+ height: 1003px;
+ max-width: none;
+}
+
+.rr-table-side-ornament {
+ position: absolute;
+ z-index: 4;
+ top: 15px;
+ width: 20px;
+ height: 965px;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.rr-table-side-ornament img {
+ position: absolute;
+ top: -15px;
+ width: 1008px;
+ height: 1003px;
+ max-width: none;
+}
+
+.rr-table-side-left {
+ left: 0;
+}
+
+.rr-table-side-left img {
+ left: 0;
+}
+
+.rr-table-side-right {
+ right: 0;
+}
+
+.rr-table-side-right img {
+ right: 0;
+}
+
+.rr-table-divider {
+ position: absolute;
+ z-index: 3;
+ top: 15px;
+ left: 503px;
+ width: 2px;
+ height: 824px;
+ background: rgba(255, 255, 255, 0.92);
+ pointer-events: none;
+}
+
+.rr-table-content {
+ position: absolute;
+ z-index: 2;
+ top: 15px;
+ left: 20px;
+ width: 968px;
+ min-height: 824px;
+}
+
+.rr-tier-row {
+ display: grid;
+ grid-template-columns: 484px 484px;
+ width: 968px;
+ height: 103px;
+ color: #fff;
+ font-size: 30px;
+ font-weight: 500;
+ line-height: 32px;
+ letter-spacing: 0;
+}
+
+.rr-tier-row:nth-child(odd) {
+ background: var(--rr-row-dark);
+}
+
+.rr-tier-row:nth-child(even) {
+ background: var(--rr-purple-bg);
+}
+
+.rr-tier-cell {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 0;
+ height: 103px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rr-tier-row-head .rr-tier-cell {
+ padding-top: 2px;
+}
+
+.rr-tier-reward {
+ gap: 8px;
+ color: #d7a8ff;
+}
+
+.rr-tier-reward img {
+ flex: 0 0 auto;
+ width: 38px;
+ height: 38px;
+ object-fit: contain;
+}
+
+.rr-tier-placeholder {
+ color: rgba(255, 255, 255, 0.42);
+}
+
+.rr-footer-shell {
+ position: fixed;
+ z-index: 80;
+ bottom: 0;
+ left: 50%;
+ width: var(--rr-page-width);
+ height: 58.681px;
+ overflow: visible;
+ transform: translateX(-50%);
+}
+
+.rr-user-panel {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 169px;
+ overflow: hidden;
+ transform: scale(var(--rr-scale));
+ transform-origin: top left;
+ border-radius: 24px 24px 0 0;
+ background: var(--rr-footer-bg);
+}
+
+.rr-avatar-shell {
+ position: absolute;
+ top: 25px;
+ left: 46px;
+ display: block;
+ width: 119px;
+ height: 119px;
+ overflow: hidden;
+ border: 3px solid #e3bbff;
+ border-radius: 50%;
+ background: #21053b;
+}
+
+.rr-avatar-shell img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: 50% 32%;
+}
+
+html.rr-mock .rr-avatar-shell img {
+ position: absolute;
+ top: -17px;
+ left: -41px;
+ width: 185px;
+ height: 246px;
+ max-width: none;
+ object-fit: fill;
+}
+
+.rr-user-name {
+ position: absolute;
+ top: 36px;
+ left: 189px;
+ width: 288px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 44px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rr-user-id {
+ position: absolute;
+ top: 102px;
+ left: 189px;
+ width: 292px;
+ overflow: hidden;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 30px;
+ font-weight: 500;
+ line-height: 36px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rr-metric-label {
+ position: absolute;
+ overflow: hidden;
+ color: #fff;
+ font-size: 28px;
+ font-weight: 500;
+ line-height: 34px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rr-contribution-label {
+ top: 42px;
+ left: 494px;
+ width: 340px;
+}
+
+.rr-rewards-label {
+ top: 110px;
+ left: 552px;
+ width: 280px;
+}
+
+.rr-value-pill {
+ position: absolute;
+ display: block;
+ width: 215px;
+ height: 69px;
+ margin: 0;
+ padding: 0;
+ overflow: visible;
+ border: 0;
+ color: #fff;
+ font: inherit;
+}
+
+.rr-value-pill > img {
+ position: absolute;
+ inset: 0;
+ width: 215px;
+ height: 69px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.rr-value-pill > span {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ padding: 0 28px;
+ color: #fff;
+ font-size: 24px;
+ font-weight: 500;
+ line-height: 28px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rr-value-pill-active {
+ top: 19px;
+ left: 838px;
+}
+
+.rr-value-pill-disabled {
+ top: 87px;
+ left: 837px;
+}
+
+.rr-sr-only {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
+html[dir='rtl'] .rr-user-name,
+html[dir='rtl'] .rr-user-id,
+html[dir='rtl'] .rr-metric-label {
+ direction: rtl;
+ text-align: right;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/activity/room-reward/fami/fami.js b/activity/room-reward/fami/fami.js
new file mode 100644
index 0000000..e4a4267
--- /dev/null
+++ b/activity/room-reward/fami/fami.js
@@ -0,0 +1,433 @@
+(function () {
+ 'use strict';
+
+ var DESIGN_WIDTH = 1080;
+ var DESIGN_HEIGHT = 1985;
+ var TABLE_TOP = 1005;
+ var TABLE_CONTENT_TOP = 1020;
+ var ROW_HEIGHT = 103;
+ var MIN_TIER_SLOTS = 7;
+ var TABLE_BOTTOM_SPACE = 141;
+ var MAX_PAGE_WIDTH = 430;
+ var ASSET_BASE = './fami/assets/layers/';
+
+ var params = new URLSearchParams(window.location.search || '');
+ var useMock = params.get('mock') === '1';
+ document.documentElement.classList.toggle('rr-mock', useMock);
+ var stageHeight = DESIGN_HEIGHT;
+ var currentData = null;
+ var loadState = 'loading';
+ var resizeFrame = 0;
+
+ var app = document.getElementById('rrApp');
+ var stageShell = document.getElementById('rrStageShell');
+ var stage = document.getElementById('rrStage');
+ var table = document.getElementById('rrTable');
+ var tierTable = document.getElementById('rrTierTable');
+ var tableDivider = document.getElementById('rrTableDivider');
+ var tableSides = Array.prototype.slice.call(
+ document.querySelectorAll('.rr-table-side-ornament')
+ );
+ var footerShell = document.getElementById('rrFooterShell');
+ var statusNode = document.getElementById('rrStatus');
+ var avatar = document.getElementById('rrUserAvatar');
+
+ var mockData = {
+ targetReward: 1000,
+ user: {
+ name: 'Namename...',
+ id: '123456789',
+ avatar: ASSET_BASE + 'avatar-mock.webp',
+ },
+ currentCoinSpent: 100,
+ expectedReward: 100,
+ tiers: Array.from({ length: 7 }, function (_, index) {
+ return {
+ id: 'figma-tier-' + String(index + 1),
+ turnover: 7000000,
+ reward: 350000,
+ };
+ }),
+ };
+
+ function t(key, fallback) {
+ if (window.HyAppI18n && window.HyAppI18n.t) {
+ return window.HyAppI18n.t(key, fallback);
+ }
+ return fallback || key;
+ }
+
+ function first(source, keys) {
+ source = source || {};
+ for (var index = 0; index < keys.length; index += 1) {
+ var value = source[keys[index]];
+ if (value !== undefined && value !== null && value !== '') {
+ return value;
+ }
+ }
+ return '';
+ }
+
+ function number(value, fallback) {
+ var parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : Number(fallback || 0);
+ }
+
+ function unwrap(payload) {
+ if (
+ payload &&
+ typeof payload === 'object' &&
+ Object.prototype.hasOwnProperty.call(payload, 'data')
+ ) {
+ return payload.data || {};
+ }
+ return payload || {};
+ }
+
+ function trimFixed(value) {
+ var fixed = value >= 10 ? value.toFixed(0) : value.toFixed(1);
+ return fixed.replace(/\.0$/, '');
+ }
+
+ function compactNumber(value) {
+ var amount = number(value, 0);
+ if (amount >= 1000000000) return trimFixed(amount / 1000000000) + 'B';
+ if (amount >= 1000000) return trimFixed(amount / 1000000) + 'M';
+ if (amount >= 1000) return trimFixed(amount / 1000) + 'k';
+ return String(Math.floor(amount));
+ }
+
+ function plainWholeNumber(value) {
+ return String(Math.floor(number(value, 0)));
+ }
+
+ function wholeNumber(value) {
+ var lang =
+ window.HyAppI18n && window.HyAppI18n.lang
+ ? window.HyAppI18n.lang()
+ : 'en';
+ return new Intl.NumberFormat(lang, {
+ maximumFractionDigits: 0,
+ }).format(Math.floor(number(value, 0)));
+ }
+
+ function normalizeTier(tier, index) {
+ tier = tier || {};
+ return {
+ id:
+ first(tier, ['tier_id', 'tierId', 'id', 'tier_code']) ||
+ 'tier-' + String(index + 1),
+ turnover: number(
+ first(tier, [
+ 'threshold_coin_spent',
+ 'thresholdCoinSpent',
+ 'turnover',
+ ]),
+ 0
+ ),
+ reward: number(
+ first(tier, [
+ 'reward_coin_amount',
+ 'rewardCoinAmount',
+ 'reward',
+ ]),
+ 0
+ ),
+ };
+ }
+
+ function normalizeAPIData(payload) {
+ var data = unwrap(payload);
+ var user = data.user || {};
+ var room = data.room || {};
+ var currentCoinSpent = number(
+ first(data, ['current_coin_spent', 'currentCoinSpent']),
+ 0
+ );
+ var expectedReward = number(
+ first(data, [
+ 'expected_reward_coin_amount',
+ 'expectedRewardCoinAmount',
+ ]),
+ 0
+ );
+ var tiers = Array.isArray(data.tiers) ? data.tiers : [];
+
+ return {
+ enabled: data.enabled !== false,
+ targetReward: expectedReward,
+ currentCoinSpent: currentCoinSpent,
+ expectedReward: expectedReward,
+ user: {
+ name:
+ first(user, ['username', 'name']) || first(room, ['title']),
+ id:
+ first(user, [
+ 'display_user_id',
+ 'displayUserId',
+ 'user_id',
+ 'userId',
+ ]) || first(room, ['owner_user_id', 'ownerUserId']),
+ avatar:
+ first(user, ['avatar']) ||
+ first(room, ['cover_url', 'coverUrl']),
+ },
+ tiers: tiers.map(normalizeTier),
+ };
+ }
+
+ function createCell(className, text) {
+ var cell = document.createElement('div');
+ cell.className = 'rr-tier-cell' + (className ? ' ' + className : '');
+ cell.textContent = text;
+ return cell;
+ }
+
+ function createHeaderRow() {
+ var row = document.createElement('div');
+ var turnover = createCell(
+ '',
+ t('roomReward.turnover', 'Room turnover')
+ );
+ var reward = createCell(
+ '',
+ t('roomReward.receiveReward', 'Receive a reward')
+ );
+ row.className = 'rr-tier-row rr-tier-row-head';
+ turnover.setAttribute('data-i18n', 'roomReward.turnover');
+ reward.setAttribute('data-i18n', 'roomReward.receiveReward');
+ row.appendChild(turnover);
+ row.appendChild(reward);
+ return row;
+ }
+
+ function createTierRow(tier) {
+ var row = document.createElement('div');
+ row.className = 'rr-tier-row';
+
+ if (!tier) {
+ row.classList.add('rr-tier-placeholder');
+ row.appendChild(createCell('', '—'));
+ row.appendChild(createCell('', '—'));
+ return row;
+ }
+
+ row.appendChild(createCell('', compactNumber(tier.turnover)));
+ var rewardCell = createCell('rr-tier-reward', '');
+ var amount = document.createElement('span');
+ var coin = document.createElement('img');
+ amount.textContent = compactNumber(tier.reward);
+ coin.src = ASSET_BASE + 'coin-tier.webp';
+ coin.alt = '';
+ coin.setAttribute('aria-hidden', 'true');
+ coin.draggable = false;
+ rewardCell.appendChild(amount);
+ rewardCell.appendChild(coin);
+ row.appendChild(rewardCell);
+ return row;
+ }
+
+ function renderTable(tiers) {
+ tiers = Array.isArray(tiers) ? tiers : [];
+ var slotCount = Math.max(tiers.length, MIN_TIER_SLOTS);
+ var fragment = document.createDocumentFragment();
+ fragment.appendChild(createHeaderRow());
+
+ for (var index = 0; index < slotCount; index += 1) {
+ fragment.appendChild(createTierRow(tiers[index] || null));
+ }
+
+ tierTable.replaceChildren(fragment);
+ tierTable.setAttribute(
+ 'aria-busy',
+ loadState === 'loading' ? 'true' : 'false'
+ );
+ syncTableLayout(slotCount + 1);
+ }
+
+ function syncTableLayout(rowCount) {
+ var rowsHeight = Math.max(rowCount, 1) * ROW_HEIGHT;
+ // The Figma frame leaves 141 design pixels after the final row. Reusing that
+ // relationship for extra API tiers keeps the fixed footer overlap identical
+ // instead of hiding an arbitrary amount of the last dynamic row.
+ stageHeight = Math.max(
+ DESIGN_HEIGHT,
+ TABLE_CONTENT_TOP + rowsHeight + TABLE_BOTTOM_SPACE
+ );
+
+ var tableHeight = stageHeight - TABLE_TOP;
+ var sideHeight = tableHeight - 15;
+ var sideImageHeight = Math.max(1003, tableHeight + 23);
+
+ document.documentElement.style.setProperty(
+ '--rr-stage-height',
+ stageHeight + 'px'
+ );
+ stage.style.height = stageHeight + 'px';
+ table.style.height = tableHeight + 'px';
+ tierTable.style.minHeight = rowsHeight + 'px';
+ tableDivider.style.height = rowsHeight + 'px';
+ tableSides.forEach(function (side) {
+ side.style.height = sideHeight + 'px';
+ var image = side.querySelector('img');
+ if (image) image.style.height = sideImageHeight + 'px';
+ });
+ syncViewport();
+ }
+
+ function userID(value) {
+ return t('roomReward.idPrefix', 'ID:') + ' ' + (value || '—');
+ }
+
+ function setAvatar(source) {
+ source = String(source || '').trim();
+ if (!source) {
+ avatar.hidden = true;
+ avatar.removeAttribute('src');
+ return;
+ }
+ avatar.hidden = false;
+ avatar.src = source;
+ }
+
+ function renderUser(data) {
+ var user = data && data.user ? data.user : {};
+ document.getElementById('rrTargetValue').textContent = data
+ ? plainWholeNumber(data.targetReward)
+ : '—';
+ document.getElementById('rrUserName').textContent = data
+ ? user.name || t('roomReward.roomOwner', 'Room owner')
+ : '—';
+ document.getElementById('rrUserId').textContent = userID(
+ data ? user.id : ''
+ );
+ document.getElementById('rrContributionValue').textContent = data
+ ? wholeNumber(data.currentCoinSpent)
+ : '—';
+ document.getElementById('rrRewardValue').textContent = data
+ ? wholeNumber(data.expectedReward)
+ : '—';
+ setAvatar(data ? user.avatar : '');
+ }
+
+ function setStatus(nextState) {
+ loadState = nextState;
+ var key = '';
+ var fallback = '';
+ if (nextState === 'loading') {
+ key = 'roomReward.loading';
+ fallback = 'Loading...';
+ } else if (nextState === 'empty') {
+ key = 'roomReward.empty';
+ fallback = 'No reward tiers yet';
+ } else if (nextState === 'error') {
+ key = 'roomReward.loadFailed';
+ fallback = 'Load failed';
+ }
+
+ if (key) {
+ statusNode.setAttribute('data-i18n', key);
+ statusNode.textContent = t(key, fallback);
+ } else {
+ statusNode.removeAttribute('data-i18n');
+ statusNode.textContent = '';
+ }
+ tierTable.setAttribute(
+ 'aria-busy',
+ nextState === 'loading' ? 'true' : 'false'
+ );
+ app.setAttribute('data-state', nextState);
+ }
+
+ function render(data) {
+ currentData = data;
+ var hasTiers = Boolean(data && data.tiers && data.tiers.length);
+ setStatus(hasTiers ? 'ready' : 'empty');
+ renderTable(data ? data.tiers : []);
+ renderUser(data);
+ }
+
+ function loadData() {
+ if (useMock) return Promise.resolve(mockData);
+
+ // Mock data is opt-in only. A production API failure must remain visible as
+ // an error state; silently substituting Figma fixture numbers would present
+ // fabricated room turnover and reward balances to the user.
+ if (!window.HyAppAPI || !window.HyAppAPI.get) {
+ return Promise.reject(new Error('room_reward_api_unavailable'));
+ }
+
+ return window.HyAppAPI.get(
+ '/api/v1/activities/room-turnover-reward'
+ ).then(normalizeAPIData);
+ }
+
+ function syncViewport() {
+ var viewportWidth =
+ document.documentElement.clientWidth || window.innerWidth || 375;
+ var pageWidth = Math.min(viewportWidth, MAX_PAGE_WIDTH);
+ var scale = pageWidth / DESIGN_WIDTH;
+ var shellHeight = stageHeight * scale;
+ var footerHeight = 169 * scale;
+
+ document.documentElement.style.setProperty('--rr-scale', String(scale));
+ document.documentElement.style.setProperty(
+ '--rr-page-width',
+ pageWidth + 'px'
+ );
+ stageShell.style.width = pageWidth + 'px';
+ stageShell.style.height = shellHeight + 'px';
+ footerShell.style.width = pageWidth + 'px';
+ footerShell.style.height = footerHeight + 'px';
+ }
+
+ function scheduleViewportSync() {
+ if (resizeFrame) window.cancelAnimationFrame(resizeFrame);
+ resizeFrame = window.requestAnimationFrame(function () {
+ resizeFrame = 0;
+ syncViewport();
+ });
+ }
+
+ function syncLanguageText() {
+ document.title = t('roomReward.pageTitle', 'Room Reward Event');
+ if (currentData) renderUser(currentData);
+ if (loadState !== 'ready') setStatus(loadState);
+ }
+
+ function init() {
+ setStatus('loading');
+ renderTable([]);
+ renderUser(null);
+ syncViewport();
+
+ avatar.addEventListener('error', function () {
+ // Broken remote media must not fall through to the Figma mock avatar in
+ // production; hiding it keeps the identity state honest and stable.
+ avatar.hidden = true;
+ });
+ window.addEventListener('resize', scheduleViewportSync, {
+ passive: true,
+ });
+ window.addEventListener('orientationchange', scheduleViewportSync, {
+ passive: true,
+ });
+ window.addEventListener('hyapp:i18n-ready', syncLanguageText);
+
+ loadData()
+ .then(render)
+ .catch(function (error) {
+ console.warn('[fami-room-reward] load failed', error);
+ currentData = null;
+ setStatus('error');
+ renderTable([]);
+ renderUser(null);
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/activity/week-star/fami.html b/activity/week-star/fami.html
new file mode 100644
index 0000000..eee95f8
--- /dev/null
+++ b/activity/week-star/fami.html
@@ -0,0 +1,422 @@
+
+
+
+
+
+ Weekly Star
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+ Weekly star
+
+
+
+ Note: Send the following three gifts to participate in
+ the ranking. Users who rank in the top 3 of the
+ leaderboard will receive rewards.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/week-star/fami/assets/fixtures/mock-avatar-top.webp b/activity/week-star/fami/assets/fixtures/mock-avatar-top.webp
new file mode 100644
index 0000000..955ec9b
Binary files /dev/null and b/activity/week-star/fami/assets/fixtures/mock-avatar-top.webp differ
diff --git a/activity/week-star/fami/assets/fixtures/mock-avatar-user.webp b/activity/week-star/fami/assets/fixtures/mock-avatar-user.webp
new file mode 100644
index 0000000..ef8adb9
Binary files /dev/null and b/activity/week-star/fami/assets/fixtures/mock-avatar-user.webp differ
diff --git a/activity/week-star/fami/assets/fixtures/mock-gift.webp b/activity/week-star/fami/assets/fixtures/mock-gift.webp
new file mode 100644
index 0000000..a1177bf
Binary files /dev/null and b/activity/week-star/fami/assets/fixtures/mock-gift.webp differ
diff --git a/activity/week-star/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2 b/activity/week-star/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2
new file mode 100644
index 0000000..8519f51
Binary files /dev/null and b/activity/week-star/fami/assets/fonts/source-han-sans-sc-medium-latin.woff2 differ
diff --git a/activity/week-star/fami/assets/layers/body-watermark.webp b/activity/week-star/fami/assets/layers/body-watermark.webp
new file mode 100644
index 0000000..1930c04
Binary files /dev/null and b/activity/week-star/fami/assets/layers/body-watermark.webp differ
diff --git a/activity/week-star/fami/assets/layers/gift-frame.webp b/activity/week-star/fami/assets/layers/gift-frame.webp
new file mode 100644
index 0000000..08c91a6
Binary files /dev/null and b/activity/week-star/fami/assets/layers/gift-frame.webp differ
diff --git a/activity/week-star/fami/assets/layers/gift-glow.webp b/activity/week-star/fami/assets/layers/gift-glow.webp
new file mode 100644
index 0000000..f4be325
Binary files /dev/null and b/activity/week-star/fami/assets/layers/gift-glow.webp differ
diff --git a/activity/week-star/fami/assets/layers/manifest.json b/activity/week-star/fami/assets/layers/manifest.json
new file mode 100644
index 0000000..2d1debc
--- /dev/null
+++ b/activity/week-star/fami/assets/layers/manifest.json
@@ -0,0 +1,632 @@
+{
+ "schemaVersion": 2,
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "canvas": {
+ "id": "721:2016",
+ "name": "Fami运营"
+ },
+ "entryFrames": {
+ "history": {
+ "id": "837:4472",
+ "name": "历史前三",
+ "width": 1080,
+ "height": 2651
+ },
+ "current": {
+ "id": "837:4545",
+ "name": "历史前三",
+ "width": 1080,
+ "height": 2460
+ },
+ "rewards": {
+ "id": "837:4615",
+ "name": "历史前三",
+ "width": 1080,
+ "height": 3532
+ }
+ },
+ "references": {
+ "hero": {
+ "groupNode": "837:4721",
+ "maskNode": "837:4722",
+ "imageNode": "837:4723",
+ "bounds": {
+ "x": 0,
+ "y": 0,
+ "width": 1080,
+ "height": 1080
+ },
+ "runtimeExported": false,
+ "purpose": "Trace the Figma hero and bottom fade only. The runtime header is decomposed into the body watermark plus DOM/CSS title, date, note, and controls."
+ }
+ }
+ },
+ "policy": {
+ "rule": "Only isolated single visual nodes are shippable assets. Root canvases, complete state frames, multi-element regions, text, controls, leaderboard rows, reward grids, and dynamic business media are never runtime screenshots.",
+ "forbiddenAggregateNodes": [
+ "721:2016",
+ "837:4472",
+ "837:4545",
+ "837:4615",
+ "837:4475",
+ "837:4491",
+ "837:4507",
+ "837:4548",
+ "837:4564",
+ "837:4579",
+ "837:4618",
+ "837:4634",
+ "837:4649",
+ "837:4721"
+ ],
+ "allowedSourceTypes": ["RECTANGLE"],
+ "forbiddenRuntimeNamePatterns": [
+ "hero",
+ "header",
+ "full-page",
+ "whole-page",
+ "leaderboard",
+ "reward-grid",
+ "frame-837"
+ ]
+ },
+ "compositions": {
+ "pageBackground": {
+ "layers": ["body-watermark.webp"],
+ "dynamicSlots": []
+ },
+ "giftCard": {
+ "layers": ["gift-frame.webp", "gift-glow.webp"],
+ "dynamicSlots": ["gift.media"]
+ },
+ "tabs": {
+ "layers": ["tab-active.webp", "tab-inactive.webp"],
+ "dynamicSlots": ["tab.label", "tab.state"]
+ },
+ "leaderboard": {
+ "layers": ["medal-1.webp", "medal-2.webp", "medal-3.webp"],
+ "dynamicSlots": [
+ "user.avatar.top",
+ "user.avatar",
+ "user.displayName",
+ "user.score"
+ ]
+ },
+ "rewardPanel": {
+ "layers": [
+ "reward-panel.webp",
+ "reward-heading.webp",
+ "gift-frame.webp",
+ "gift-glow.webp"
+ ],
+ "dynamicSlots": [
+ "reward.rank",
+ "reward.giftMedia",
+ "reward.duration"
+ ]
+ }
+ },
+ "fixtureBindings": [
+ {
+ "slot": "gift.media",
+ "file": "../fixtures/mock-gift.webp",
+ "mockOnly": true
+ },
+ {
+ "slot": "user.avatar.top",
+ "file": "../fixtures/mock-avatar-top.webp",
+ "mockOnly": true
+ },
+ {
+ "slot": "user.avatar",
+ "file": "../fixtures/mock-avatar-user.webp",
+ "mockOnly": true
+ }
+ ],
+ "assets": [
+ {
+ "file": "body-watermark.webp",
+ "scope": "runtime",
+ "kind": "single-raster-image-fill",
+ "sourceNode": "837:4544",
+ "equivalentNodes": ["837:4614", "837:4718"],
+ "sourceName": "image 5022",
+ "sourceType": "RECTANGLE",
+ "purpose": "Text-free palace background reused by the decomposed Hero and as a low-opacity watermark behind DOM leaderboard and reward content",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": -65,
+ "y": 987,
+ "width": 1210,
+ "height": 2689
+ },
+ "original": {
+ "width": 841,
+ "height": 1870
+ },
+ "exported": {
+ "width": 841,
+ "height": 1870
+ },
+ "figmaOpacity": 0.1,
+ "exportFormat": "webp-lossy",
+ "quality": 88,
+ "alphaQuality": null,
+ "hasAlpha": false,
+ "bytes": 605038,
+ "sha256": "b09813ba3587478745f587dd2f7c599cf7e35abf8ca957525d7c1e8fbb8f9e8b",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "gift-frame.webp",
+ "scope": "runtime",
+ "kind": "single-raster-ornamental-frame",
+ "sourceNode": "837:4479",
+ "equivalentNodes": ["837:4661"],
+ "sourceName": "058 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Green-gold gift frame under live gift media",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 61,
+ "y": 1247,
+ "width": 257,
+ "height": 258
+ },
+ "original": {
+ "width": 1169,
+ "height": 1175
+ },
+ "exported": {
+ "width": 514,
+ "height": 516
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 86,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 40204,
+ "sha256": "37842036b84402bb1b43a091b023a00a4221a442c0b0b9c882f864dc034af980",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "gift-glow.webp",
+ "scope": "runtime",
+ "kind": "single-raster-ornamental-overlay",
+ "sourceNode": "837:4662",
+ "equivalentNodes": [],
+ "sourceName": "电商双十一通用弹窗广告_设计页-稿定设计 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Soft radial glow layered between the gift frame and live gift media",
+ "bounds": {
+ "relativeTo": "837:4615",
+ "x": 153.5,
+ "y": 2041,
+ "width": 219,
+ "height": 219
+ },
+ "original": {
+ "width": 1183,
+ "height": 1183
+ },
+ "exported": {
+ "width": 438,
+ "height": 438
+ },
+ "figmaOpacity": 0.34,
+ "exportFormat": "webp-lossy",
+ "quality": 84,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 33306,
+ "sha256": "06a7cb9da121e77a8e938c98c35ccfacdf5542cac92ec64dcc32bd2eec729c16",
+ "dependencies": ["gift-frame.webp"],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "tab-active.webp",
+ "scope": "runtime",
+ "kind": "single-raster-tab-skin",
+ "sourceNode": "837:4493",
+ "equivalentNodes": ["837:4569", "837:4642"],
+ "sourceName": "图层 0 36",
+ "sourceType": "RECTANGLE",
+ "purpose": "Selected tab skin; tab labels and click targets remain DOM",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 21,
+ "y": 1557,
+ "width": 335,
+ "height": 149
+ },
+ "original": {
+ "width": 1567,
+ "height": 697
+ },
+ "exported": {
+ "width": 672,
+ "height": 298
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 86,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 32938,
+ "sha256": "34d4346275416d33c5cf670245f873f710d120531bd9a94d773cca43f3d79a97",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "tab-inactive.webp",
+ "scope": "runtime",
+ "kind": "single-raster-tab-skin",
+ "sourceNode": "837:4496",
+ "equivalentNodes": [
+ "837:4499",
+ "837:4566",
+ "837:4572",
+ "837:4636",
+ "837:4639"
+ ],
+ "sourceName": "二二 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Unselected tab skin; tab labels and click targets remain DOM",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 374,
+ "y": 1559,
+ "width": 336,
+ "height": 149
+ },
+ "original": {
+ "width": 1024,
+ "height": 455
+ },
+ "exported": {
+ "width": 672,
+ "height": 298
+ },
+ "figmaOpacity": 1,
+ "paintAdjustments": {
+ "saturation": -0.49
+ },
+ "exportFormat": "webp-lossy",
+ "quality": 86,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 22864,
+ "sha256": "07603c2c22b4fdc22efe1554049928de593f47ccd0cfb0c78b3d268e4c190b9e",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "medal-1.webp",
+ "scope": "runtime",
+ "kind": "single-raster-rank-medal",
+ "sourceNode": "837:4519",
+ "equivalentNodes": ["837:4589"],
+ "sourceName": "图层 2 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Rank-one medal beside live leaderboard identity data",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 45,
+ "y": 1939,
+ "width": 69,
+ "height": 61
+ },
+ "original": {
+ "width": 625,
+ "height": 553
+ },
+ "exported": {
+ "width": 207,
+ "height": 183
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 90,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 11526,
+ "sha256": "16122fdee746330822feeb64abe143100c25ccb706e50eb6cbea5a0c197fb87a",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "medal-2.webp",
+ "scope": "runtime",
+ "kind": "single-raster-rank-medal",
+ "sourceNode": "837:4524",
+ "equivalentNodes": ["837:4594"],
+ "sourceName": "图层 2 2",
+ "sourceType": "RECTANGLE",
+ "purpose": "Rank-two medal beside live leaderboard identity data",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 45,
+ "y": 2079,
+ "width": 69,
+ "height": 61
+ },
+ "original": {
+ "width": 642,
+ "height": 582
+ },
+ "exported": {
+ "width": 207,
+ "height": 188
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 90,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 10310,
+ "sha256": "6fd3956dcace1ff7646a3207ac68a23a33dd095ca597e7b62df5b60be07552b7",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "medal-3.webp",
+ "scope": "runtime",
+ "kind": "single-raster-rank-medal",
+ "sourceNode": "837:4531",
+ "equivalentNodes": [],
+ "sourceName": "图层 4 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Rank-three medal beside live leaderboard identity data",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 47,
+ "y": 2219,
+ "width": 63,
+ "height": 61
+ },
+ "original": {
+ "width": 619,
+ "height": 595
+ },
+ "exported": {
+ "width": 207,
+ "height": 199
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 90,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 11086,
+ "sha256": "2a5984f4166e0b832b74f4753db52b5e6dd59061a16698710e10aa9f020876c1",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "reward-panel.webp",
+ "scope": "runtime",
+ "kind": "single-raster-panel-skin",
+ "sourceNode": "837:4650",
+ "equivalentNodes": [],
+ "sourceName": "图层 0 37",
+ "sourceType": "RECTANGLE",
+ "purpose": "Empty reward panel skin behind DOM reward groups",
+ "bounds": {
+ "relativeTo": "837:4615",
+ "x": 24,
+ "y": 1760,
+ "width": 1033,
+ "height": 1689
+ },
+ "original": {
+ "width": 931,
+ "height": 1522
+ },
+ "exported": {
+ "width": 931,
+ "height": 1522
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 85,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 98298,
+ "sha256": "96f8029823e50297c46cec97809ec255ae040879df58a32c9d5a6f28acd56983",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "reward-heading.webp",
+ "scope": "runtime",
+ "kind": "single-raster-node-crop",
+ "sourceNode": "837:4653",
+ "equivalentNodes": ["837:4675", "837:4697"],
+ "sourceName": "2323a16b-9cbe-4acd-b676-8fd51cd1db70 1",
+ "sourceType": "RECTANGLE",
+ "purpose": "Cropped empty reward rank heading bar; TOP labels remain DOM text",
+ "bounds": {
+ "relativeTo": "837:4615",
+ "x": 225,
+ "y": 1876,
+ "width": 632,
+ "height": 98
+ },
+ "original": {
+ "width": 632,
+ "height": 98
+ },
+ "exported": {
+ "width": 632,
+ "height": 98
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossy",
+ "quality": 90,
+ "alphaQuality": 100,
+ "hasAlpha": true,
+ "bytes": 11798,
+ "sha256": "41fb59bae1e6800e95a712772179c49697129590fc99bb9a43f7e781a8c5c9de",
+ "dependencies": ["reward-panel.webp"],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": false,
+ "mockOnly": false,
+ "dynamic": false
+ },
+ {
+ "file": "../fixtures/mock-gift.webp",
+ "scope": "fixture",
+ "kind": "single-dynamic-media-fixture",
+ "sourceNode": "837:4480",
+ "equivalentNodes": ["837:4663"],
+ "sourceName": "10",
+ "sourceType": "RECTANGLE",
+ "purpose": "Figma sample gift used only by an explicit mock or visual-QA mode",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 96,
+ "y": 1288,
+ "width": 188,
+ "height": 188
+ },
+ "original": {
+ "width": 144,
+ "height": 144
+ },
+ "exported": {
+ "width": 144,
+ "height": 144
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossless",
+ "quality": null,
+ "alphaQuality": null,
+ "hasAlpha": true,
+ "bytes": 16014,
+ "sha256": "6c222a35801df12a90627e69eaf3945e268003be34b745d81357f22b1cf98c75",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": true,
+ "mockOnly": true,
+ "dynamic": true
+ },
+ {
+ "file": "../fixtures/mock-avatar-top.webp",
+ "scope": "fixture",
+ "kind": "single-dynamic-media-fixture",
+ "sourceNode": "837:4518",
+ "equivalentNodes": [],
+ "sourceName": "Rectangle 34",
+ "sourceType": "RECTANGLE",
+ "purpose": "Figma first-place avatar used only by an explicit mock or visual-QA mode",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 160,
+ "y": 1913,
+ "width": 113,
+ "height": 113
+ },
+ "original": {
+ "width": 1200,
+ "height": 1200
+ },
+ "exported": {
+ "width": 1200,
+ "height": 1200
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossless",
+ "quality": null,
+ "alphaQuality": null,
+ "hasAlpha": false,
+ "bytes": 1476722,
+ "sha256": "3740a7decc5288288b1b1e52393ea7a76a633ac7832764a06dab36cd44d66af0",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": true,
+ "mockOnly": true,
+ "dynamic": true
+ },
+ {
+ "file": "../fixtures/mock-avatar-user.webp",
+ "scope": "fixture",
+ "kind": "single-dynamic-media-fixture",
+ "sourceNode": "837:4523",
+ "equivalentNodes": [],
+ "sourceName": "Rectangle 35",
+ "sourceType": "RECTANGLE",
+ "purpose": "Figma leaderboard avatar used only by an explicit mock or visual-QA mode",
+ "bounds": {
+ "relativeTo": "837:4472",
+ "x": 160,
+ "y": 2053,
+ "width": 113,
+ "height": 113
+ },
+ "original": {
+ "width": 768,
+ "height": 1024
+ },
+ "exported": {
+ "width": 768,
+ "height": 1024
+ },
+ "figmaOpacity": 1,
+ "exportFormat": "webp-lossless",
+ "quality": null,
+ "alphaQuality": null,
+ "hasAlpha": false,
+ "bytes": 911586,
+ "sha256": "b23c230ada8cbc217ae6b238dd2df2efb9d46d79ea4b9b7e8a36e7f4230eaf3b",
+ "dependencies": [],
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicMedia": true,
+ "mockOnly": true,
+ "dynamic": true
+ }
+ ]
+}
diff --git a/activity/week-star/fami/assets/layers/medal-1.webp b/activity/week-star/fami/assets/layers/medal-1.webp
new file mode 100644
index 0000000..b668897
Binary files /dev/null and b/activity/week-star/fami/assets/layers/medal-1.webp differ
diff --git a/activity/week-star/fami/assets/layers/medal-2.webp b/activity/week-star/fami/assets/layers/medal-2.webp
new file mode 100644
index 0000000..64c95fc
Binary files /dev/null and b/activity/week-star/fami/assets/layers/medal-2.webp differ
diff --git a/activity/week-star/fami/assets/layers/medal-3.webp b/activity/week-star/fami/assets/layers/medal-3.webp
new file mode 100644
index 0000000..16d598d
Binary files /dev/null and b/activity/week-star/fami/assets/layers/medal-3.webp differ
diff --git a/activity/week-star/fami/assets/layers/reward-heading.webp b/activity/week-star/fami/assets/layers/reward-heading.webp
new file mode 100644
index 0000000..368429e
Binary files /dev/null and b/activity/week-star/fami/assets/layers/reward-heading.webp differ
diff --git a/activity/week-star/fami/assets/layers/reward-panel.webp b/activity/week-star/fami/assets/layers/reward-panel.webp
new file mode 100644
index 0000000..21bacb1
Binary files /dev/null and b/activity/week-star/fami/assets/layers/reward-panel.webp differ
diff --git a/activity/week-star/fami/assets/layers/tab-active.webp b/activity/week-star/fami/assets/layers/tab-active.webp
new file mode 100644
index 0000000..f9e3e1a
Binary files /dev/null and b/activity/week-star/fami/assets/layers/tab-active.webp differ
diff --git a/activity/week-star/fami/assets/layers/tab-inactive.webp b/activity/week-star/fami/assets/layers/tab-inactive.webp
new file mode 100644
index 0000000..d3a62a8
Binary files /dev/null and b/activity/week-star/fami/assets/layers/tab-inactive.webp differ
diff --git a/activity/week-star/fami/fami.css b/activity/week-star/fami/fami.css
new file mode 100644
index 0000000..7b8ff2d
--- /dev/null
+++ b/activity/week-star/fami/fami.css
@@ -0,0 +1,925 @@
+@font-face {
+ font-family: 'Source Han Sans SC Fami Week Star';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('./assets/fonts/source-han-sans-sc-medium-latin.woff2')
+ format('woff2');
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+:root {
+ --fami-week-page-width: min(100vw, 430px);
+}
+
+html,
+body {
+ margin: 0;
+ width: 100%;
+ min-height: 100%;
+ overflow-x: hidden;
+ background: var(--hy-theme-bg, #f7f3ff);
+ color: #fff;
+ font-family:
+ 'Source Han Sans SC Fami Week Star', 'Source Han Sans SC',
+ 'Noto Sans Arabic', Arial, 'Helvetica Neue', Helvetica, sans-serif;
+}
+
+body {
+ background: #032b29;
+}
+
+button {
+ margin: 0;
+ border: 0;
+ padding: 0;
+ color: inherit;
+ font: inherit;
+ background: transparent;
+ cursor: pointer;
+}
+
+button:focus-visible {
+ outline: 6px solid #fff;
+ outline-offset: -10px;
+}
+
+img {
+ display: block;
+ max-width: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.fami-visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
+.fami-week-star {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ min-height: 100svh;
+ overflow: hidden;
+ background: #032b29;
+}
+
+.fami-stage-wrap {
+ --design-scale: 0.3472222222;
+ position: relative;
+ flex: 0 0 auto;
+ width: var(--fami-week-page-width);
+ height: 921px;
+ overflow: hidden;
+ background: #053633;
+}
+
+.fami-design {
+ position: absolute;
+ isolation: isolate;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 3532px;
+ overflow: hidden;
+ transform: scale(var(--design-scale));
+ transform-origin: left top;
+ background: #053633;
+}
+
+.fami-hero {
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 1080px;
+ overflow: hidden;
+ background: #053633;
+}
+
+.fami-hero::after {
+ position: absolute;
+ z-index: 1;
+ inset: 0;
+ background: linear-gradient(
+ 180deg,
+ rgba(1, 35, 32, 0.02) 0%,
+ rgba(1, 35, 32, 0.05) 70%,
+ #053633 100%
+ );
+ content: '';
+ pointer-events: none;
+}
+
+.fami-hero-background {
+ position: absolute;
+ z-index: 0;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 2402px;
+ object-fit: fill;
+ object-position: center top;
+ filter: saturate(1.08) brightness(1.03);
+}
+
+/* Figma 的 Hero 是压平的大区块,不能作为运行时截图;这里只复用无文字背景,星徽、标题、猫头鹰和日期牌全部独立构建。 */
+.fami-hero-star,
+.fami-hero-star::before,
+.fami-hero-star::after,
+.fami-hero-star-core,
+.fami-hero-star-core::before {
+ position: absolute;
+ display: block;
+ clip-path: polygon(
+ 50% 0,
+ 61.5% 34%,
+ 98% 35%,
+ 68.5% 56.5%,
+ 79.5% 93%,
+ 50% 71.5%,
+ 20.5% 93%,
+ 31.5% 56.5%,
+ 2% 35%,
+ 38.5% 34%
+ );
+}
+
+.fami-hero-star {
+ z-index: 2;
+ top: 75px;
+ left: 221px;
+ width: 638px;
+ height: 600px;
+ filter: drop-shadow(0 10px 4px rgba(72, 35, 0, 0.82))
+ drop-shadow(0 0 30px rgba(255, 213, 89, 0.92));
+}
+
+.fami-hero-star::before {
+ z-index: 0;
+ inset: 0;
+ background: conic-gradient(
+ from -32deg at 50% 48%,
+ #7a3c00,
+ #ffe88d 8%,
+ #e38b09 18%,
+ #fff1a7 29%,
+ #a84d00 43%,
+ #ffd85a 58%,
+ #8b4200 72%,
+ #fff0a0 88%,
+ #7a3c00
+ );
+ content: '';
+}
+
+.fami-hero-star::after {
+ z-index: 1;
+ inset: 28px;
+ background: linear-gradient(
+ 145deg,
+ #fff5a5,
+ #bb6100 35%,
+ #ffdb55 70%,
+ #713100
+ );
+ content: '';
+}
+
+.fami-hero-star-core {
+ z-index: 2;
+ inset: 57px;
+ background: url('./assets/layers/body-watermark.webp') -523px -683px /
+ 1573px 3497px no-repeat;
+ filter: saturate(1.08) contrast(1.05);
+}
+
+.fami-hero-star-core::before {
+ inset: 0;
+ background:
+ linear-gradient(
+ 90deg,
+ transparent 39%,
+ rgba(255, 246, 164, 0.08) 44%,
+ rgba(255, 255, 235, 0.88) 49.5%,
+ #fff 50%,
+ rgba(255, 255, 235, 0.88) 50.5%,
+ rgba(255, 246, 164, 0.08) 56%,
+ transparent 61%
+ ),
+ linear-gradient(
+ 0deg,
+ transparent 39%,
+ rgba(255, 246, 164, 0.08) 44%,
+ rgba(255, 255, 235, 0.88) 49.5%,
+ #fff 50%,
+ rgba(255, 255, 235, 0.88) 50.5%,
+ rgba(255, 246, 164, 0.08) 56%,
+ transparent 61%
+ ),
+ radial-gradient(
+ circle at 50% 50%,
+ #fff 0 1.5%,
+ #fffbd2 2% 4%,
+ rgba(255, 236, 122, 0.95) 5% 8%,
+ rgba(63, 255, 226, 0.48) 9% 21%,
+ transparent 37%
+ );
+ content: '';
+}
+
+.fami-title-plaque {
+ position: absolute;
+ z-index: 3;
+ top: 574px;
+ left: 57px;
+ width: 966px;
+ height: 252px;
+ border: 14px solid #e7ac31;
+ border-radius: 114px 114px 72px 72px;
+ background:
+ radial-gradient(
+ circle at 50% 110%,
+ rgba(37, 255, 227, 0.25),
+ transparent 44%
+ ),
+ linear-gradient(180deg, #0a776b 0%, #07594f 45%, #033b36 100%);
+ box-shadow:
+ inset 0 0 0 7px #fff0a4,
+ inset 0 0 0 15px #865206,
+ inset 0 0 42px rgba(0, 255, 220, 0.34),
+ 0 8px 0 #6d3e00,
+ 0 0 40px rgba(255, 202, 73, 0.65);
+}
+
+.fami-title-plaque::before,
+.fami-title-plaque::after,
+.fami-title-gem,
+.fami-date-plaque::after {
+ position: absolute;
+ width: 48px;
+ height: 48px;
+ border: 7px solid #f9cf61;
+ background: linear-gradient(135deg, #64fff1, #00a99a 55%, #006a63);
+ box-shadow:
+ inset 0 0 0 5px #0a6d65,
+ 0 0 18px rgba(67, 255, 231, 0.72);
+ content: '';
+ transform: rotate(45deg);
+}
+
+.fami-title-plaque::before {
+ top: 86px;
+ left: -32px;
+}
+
+.fami-title-plaque::after {
+ top: 86px;
+ right: -32px;
+}
+
+.fami-title-gem {
+ bottom: -32px;
+ left: calc(50% - 24px);
+}
+
+.fami-wordmark {
+ position: absolute;
+ z-index: 6;
+ top: 618px;
+ left: 115px;
+ width: 850px;
+ height: 152px;
+ margin: 0;
+ color: #ffe08a;
+ font-family: Georgia, 'Times New Roman', serif;
+ font-size: 124px;
+ font-weight: 700;
+ line-height: 152px;
+ text-align: center;
+ text-shadow:
+ 0 4px 0 #7b4300,
+ 0 8px 7px rgba(48, 25, 0, 0.75),
+ 0 0 18px #fff1a4,
+ 0 0 30px #ffbe36;
+ letter-spacing: -3px;
+ white-space: nowrap;
+ -webkit-text-stroke: 2px #fff0ad;
+}
+
+:lang(tr) .fami-wordmark,
+:lang(es) .fami-wordmark {
+ font-size: 98px;
+ letter-spacing: -2px;
+}
+
+.fami-owl {
+ position: absolute;
+ z-index: 6;
+ top: 705px;
+ left: 125px;
+ width: 286px;
+ height: 286px;
+ filter: drop-shadow(0 12px 10px rgba(0, 0, 0, 0.34));
+}
+
+.fami-date-plaque {
+ position: absolute;
+ z-index: 5;
+ top: 850px;
+ left: 357px;
+ width: 643px;
+ height: 104px;
+ border: 10px solid #e8af2d;
+ border-radius: 50px;
+ background:
+ radial-gradient(
+ circle at 50% 130%,
+ rgba(68, 255, 228, 0.3),
+ transparent 52%
+ ),
+ linear-gradient(180deg, #0a756a, #04433e);
+ box-shadow:
+ inset 0 0 0 5px #fff0a4,
+ inset 0 0 0 11px #784507,
+ 0 7px 0 #714000,
+ 0 0 24px rgba(255, 206, 76, 0.52);
+}
+
+.fami-date-plaque::after {
+ top: 21px;
+ right: -29px;
+ width: 40px;
+ height: 40px;
+}
+
+.fami-cycle-range {
+ position: absolute;
+ z-index: 7;
+ top: 889px;
+ left: 405px;
+ width: 445px;
+ height: 34px;
+ overflow: visible;
+ color: #dffbff;
+ font-family: 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 30px;
+ font-weight: 500;
+ line-height: 30px;
+ text-align: left;
+ white-space: nowrap;
+ text-shadow: 0 2px 7px rgba(0, 0, 0, 0.5);
+}
+
+.fami-note {
+ position: absolute;
+ z-index: 6;
+ top: 1069px;
+ left: 44px;
+ width: 952px;
+ height: 132px;
+ margin: 0;
+ overflow: hidden;
+ color: #e8fffd;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 42px;
+ text-align: left;
+}
+
+[dir='rtl'] .fami-note {
+ text-align: right;
+}
+
+.fami-gifts {
+ position: absolute;
+ z-index: 6;
+ top: 1247px;
+ left: 61px;
+ display: grid;
+ grid-template-columns: repeat(3, 257px);
+ column-gap: 93px;
+ width: 957px;
+ height: 258px;
+}
+
+.fami-gifts.has-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.fami-current-state,
+.fami-reward-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 36px 72px;
+ color: rgba(232, 255, 253, 0.86);
+ font-size: 34px;
+ font-weight: 500;
+ line-height: 46px;
+ text-align: center;
+ background: rgba(10, 65, 62, 0.88);
+}
+
+.fami-current-state {
+ width: 957px;
+ height: 180px;
+}
+
+.fami-gift-card {
+ position: relative;
+ width: 257px;
+ height: 258px;
+}
+
+.fami-gift-frame,
+.fami-reward-frame {
+ position: absolute;
+ z-index: 2;
+ inset: 0;
+ width: 257px;
+ height: 258px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.fami-gift-media,
+.fami-reward-media {
+ position: absolute;
+ z-index: 3;
+ top: 41px;
+ left: 35px;
+ display: grid;
+ place-items: center;
+ width: 188px;
+ height: 188px;
+}
+
+.fami-gift-media img,
+.fami-reward-media img {
+ position: relative;
+ z-index: 2;
+ width: 188px;
+ height: 188px;
+ object-fit: contain;
+}
+
+.fami-gift-media img:not([src]),
+.fami-reward-media img:not([src]) {
+ display: none;
+}
+
+.fami-media-placeholder {
+ position: absolute;
+ z-index: 1;
+ top: 50%;
+ left: 50%;
+ width: 74px;
+ height: 74px;
+ border: 8px solid rgba(255, 224, 117, 0.76);
+ background: linear-gradient(135deg, #62fff0, #038e84);
+ box-shadow: 0 0 24px rgba(82, 255, 233, 0.72);
+ transform: translate(-50%, -50%) rotate(45deg);
+}
+
+.fami-gift-media img[src] + .fami-media-placeholder,
+.fami-reward-media img[src] + .fami-media-placeholder {
+ display: none;
+}
+
+.fami-tabs {
+ position: absolute;
+ z-index: 8;
+ top: 1557px;
+ left: 21px;
+ display: grid;
+ grid-template-columns: repeat(3, 336px);
+ column-gap: 17px;
+ width: 1042px;
+ height: 151px;
+}
+
+.fami-tab {
+ position: relative;
+ width: 336px;
+ height: 151px;
+ overflow: hidden;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.fami-tab-skin {
+ position: absolute;
+ z-index: 0;
+ top: 0;
+ left: 0;
+ width: 336px;
+ height: 149px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.fami-tab-skin--active {
+ display: none;
+}
+
+.fami-tab.is-active {
+ color: #fff;
+}
+
+.fami-tab.is-active .fami-tab-skin--active {
+ display: block;
+}
+
+.fami-tab.is-active .fami-tab-skin--inactive {
+ display: none;
+}
+
+.fami-tab span {
+ position: absolute;
+ z-index: 2;
+ top: 53px;
+ left: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 270px;
+ height: 80px;
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 34px;
+ text-align: center;
+ white-space: normal;
+ text-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
+}
+
+:lang(tr) .fami-tab span,
+:lang(es) .fami-tab span {
+ top: 47px;
+ height: 90px;
+ font-size: 28px;
+ line-height: 30px;
+}
+
+.fami-panel {
+ position: absolute;
+ z-index: 5;
+ top: 0;
+ left: 0;
+ width: 1080px;
+}
+
+.fami-history-heading-band {
+ position: absolute;
+ top: 1760px;
+ left: 0;
+ width: 1080px;
+ height: 140px;
+ background: #0a413e;
+}
+
+.fami-history-heading-band h2 {
+ position: absolute;
+ top: 47px;
+ left: 120px;
+ width: 840px;
+ height: 56px;
+ margin: 0;
+ overflow: hidden;
+ color: #56ffdd;
+ font-size: 46px;
+ font-weight: 500;
+ line-height: 46px;
+ text-align: center;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ white-space: nowrap;
+}
+
+.fami-rank-list {
+ position: absolute;
+ left: 0;
+ width: 1080px;
+}
+
+.fami-rank-list--history {
+ top: 1900px;
+}
+
+.fami-rank-list--weekly {
+ top: 1760px;
+}
+
+.fami-rank-row {
+ position: relative;
+ width: 1080px;
+ height: 140px;
+ color: #fff;
+}
+
+.fami-rank-list--history .fami-rank-row:nth-child(odd) {
+ background: rgba(0, 203, 179, 0.2);
+}
+
+.fami-rank-list--history .fami-rank-row:nth-child(even),
+.fami-rank-list--weekly .fami-rank-row:nth-child(odd) {
+ background: #0a413e;
+}
+
+.fami-rank-list--weekly .fami-rank-row:nth-child(even) {
+ background: rgba(11, 47, 59, 0.2);
+}
+
+.fami-rank-position {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 137px;
+ height: 140px;
+}
+
+.fami-rank-medal {
+ position: absolute;
+ top: 39px;
+ left: 45px;
+ width: 69px;
+ height: 61px;
+ object-fit: contain;
+}
+
+.fami-rank-medal:not([src]) {
+ display: none;
+}
+
+.fami-rank-number {
+ position: absolute;
+ top: 49px;
+ left: 69px;
+ min-width: 42px;
+ height: 48px;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 42px;
+ text-align: left;
+}
+
+.fami-rank-medal[src] + .fami-rank-number {
+ display: none;
+}
+
+.fami-rank-avatar {
+ position: absolute;
+ top: 13px;
+ left: 160px;
+ width: 113px;
+ height: 113px;
+ overflow: hidden;
+ border-radius: 50%;
+ background: linear-gradient(145deg, #0d7b70, #042e2b);
+ box-shadow: inset 0 0 0 3px rgba(126, 255, 235, 0.35);
+}
+
+.fami-rank-avatar img {
+ position: absolute;
+ z-index: 2;
+ inset: 0;
+ width: 113px;
+ height: 113px;
+ object-fit: cover;
+}
+
+.fami-rank-avatar img:not([src]) {
+ display: none;
+}
+
+.fami-avatar-placeholder {
+ position: absolute;
+ z-index: 1;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ color: rgba(232, 255, 253, 0.85);
+ font-size: 42px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.fami-rank-name,
+.fami-rank-score {
+ position: absolute;
+ top: 49px;
+ height: 48px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 42px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.fami-rank-name {
+ left: 313px;
+ width: 440px;
+ text-align: start;
+}
+
+.fami-rank-score {
+ left: 846px;
+ width: 180px;
+ text-align: left;
+}
+
+.fami-list-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1080px;
+ height: 280px;
+ padding: 40px 90px;
+ color: rgba(232, 255, 253, 0.82);
+ font-size: 34px;
+ font-weight: 500;
+ line-height: 46px;
+ text-align: center;
+ background: #0a413e;
+}
+
+.fami-panel--rewards {
+ height: 3532px;
+}
+
+.fami-reward-panel-skin {
+ position: absolute;
+ top: 1760px;
+ left: 24px;
+ width: 1033px;
+ height: 1689px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.fami-reward-groups {
+ position: absolute;
+ top: 1760px;
+ left: 0;
+ width: 1080px;
+ height: 1689px;
+}
+
+.fami-reward-state {
+ position: absolute;
+ top: 300px;
+ left: 90px;
+ width: 900px;
+ min-height: 200px;
+}
+
+.fami-reward-group {
+ position: absolute;
+ top: 116px;
+ left: 0;
+ width: 1080px;
+ height: 461px;
+}
+
+.fami-reward-group:nth-child(2) {
+ top: 614px;
+}
+
+.fami-reward-group:nth-child(3) {
+ top: 1112px;
+}
+
+.fami-reward-heading {
+ position: absolute;
+ top: 0;
+ left: 225px;
+ width: 632px;
+ height: 98px;
+}
+
+.fami-reward-heading img {
+ position: absolute;
+ inset: 0;
+ width: 632px;
+ height: 98px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.fami-reward-heading h2 {
+ position: absolute;
+ top: 43px;
+ left: 120px;
+ width: 392px;
+ height: 45px;
+ margin: 0;
+ color: #dffcff;
+ font-family: 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 40px;
+ text-align: center;
+ text-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+}
+
+.fami-reward-items {
+ position: absolute;
+ top: 135px;
+ left: 134px;
+ display: grid;
+ grid-template-columns: repeat(3, 257px);
+ column-gap: 21px;
+ width: 813px;
+ height: 326px;
+}
+
+.fami-reward-items[data-count='1'] {
+ left: 412px;
+ grid-template-columns: 257px;
+ width: 257px;
+}
+
+.fami-reward-items[data-count='2'] {
+ left: 273px;
+ grid-template-columns: repeat(2, 257px);
+ width: 535px;
+}
+
+.fami-reward-item {
+ position: relative;
+ width: 257px;
+ height: 326px;
+}
+
+.fami-reward-card {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 257px;
+ height: 258px;
+}
+
+.fami-reward-glow {
+ position: absolute;
+ z-index: 1;
+ top: 30px;
+ left: 19px;
+ width: 219px;
+ height: 219px;
+ object-fit: contain;
+ opacity: 0.34;
+ pointer-events: none;
+}
+
+.fami-reward-label {
+ position: absolute;
+ top: 290px;
+ left: 3px;
+ width: 251px;
+ height: 45px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 36px;
+ text-align: center;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.fami-watermark {
+ position: absolute;
+ z-index: 1;
+ top: 987px;
+ left: -65px;
+ width: 1210px;
+ height: 2689px;
+ object-fit: fill;
+ opacity: 0.1;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/activity/week-star/fami/fami.js b/activity/week-star/fami/fami.js
new file mode 100644
index 0000000..a0aa7e6
--- /dev/null
+++ b/activity/week-star/fami/fami.js
@@ -0,0 +1,872 @@
+(function () {
+ 'use strict';
+
+ var DESIGN_WIDTH = 1080;
+ var DESIGN_HEIGHTS = {
+ history: 2651,
+ weekly: 2460,
+ rewards: 3532,
+ };
+ // Weekly Star 的公开 DTO 只提供 UTC epoch,不携带展示时区;Fami 设计与产品文案明确固定 UTC+3,
+ // 因此集中维护 +180 分钟展示规则,不能根据浏览器本地时区推断,否则同一周期会在不同国家显示不同日期。
+ var DISPLAY_OFFSET_MINUTES = 180;
+ var MAX_VISIBLE_RANKS = 5;
+ var ASSET_BASE = './fami/assets/';
+ var LAYER_BASE = ASSET_BASE + 'layers/';
+ var FIXTURE_BASE = ASSET_BASE + 'fixtures/';
+
+ var root = document.getElementById('famiWeekStar');
+ var stageWrap = document.getElementById('famiStageWrap');
+ var cycleRangeNode = document.getElementById('famiCycleRange');
+ var historyRangeNode = document.getElementById('famiHistoryRange');
+ var giftListNode = document.getElementById('famiGiftList');
+ var weeklyListNode = document.getElementById('famiWeeklyList');
+ var historyListNode = document.getElementById('famiHistoryList');
+ var rewardGroupsNode = document.getElementById('famiRewardGroups');
+ var tabs = array(document.querySelectorAll('[data-tab-target]'));
+ var panels = array(document.querySelectorAll('[role="tabpanel"]'));
+ var giftTemplate = document.getElementById('famiGiftTemplate');
+ var rankTemplate = document.getElementById('famiRankTemplate');
+ var rewardGroupTemplate = document.getElementById(
+ 'famiRewardGroupTemplate'
+ );
+ var rewardItemTemplate = document.getElementById('famiRewardItemTemplate');
+ var params = new URLSearchParams(window.location.search || '');
+ // 设计示例头像/礼物只在显式 mock=1 的验收入口使用;生产接口失败必须展示错误态,不能拿示例业务媒体伪装真实数据。
+ var useMock = params.get('mock') === '1';
+
+ var state = {
+ tab: normalizeTab(params.get('tab') || window.location.hash.slice(1)),
+ currentLoading: true,
+ weeklyLoading: true,
+ historyLoading: true,
+ current: null,
+ weeklyEntries: [],
+ historyCycles: [],
+ currentError: '',
+ historyError: '',
+ };
+
+ function array(value) {
+ return Array.prototype.slice.call(value || []);
+ }
+
+ function text(value) {
+ return String(
+ value === undefined || value === null ? '' : value
+ ).trim();
+ }
+
+ function number(value, fallback) {
+ var result = Number(value);
+ return Number.isFinite(result) ? result : Number(fallback || 0);
+ }
+
+ function first(source, keys, fallback) {
+ source = source || {};
+ for (var index = 0; index < keys.length; index += 1) {
+ var value = source[keys[index]];
+ if (value !== undefined && value !== null && value !== '') {
+ return value;
+ }
+ }
+ return fallback;
+ }
+
+ function firstObject(source, keys) {
+ var value = first(source, keys, null);
+ return value && typeof value === 'object' ? value : {};
+ }
+
+ function firstArray(source, keys) {
+ var value = first(source, keys, null);
+ return Array.isArray(value) ? value : [];
+ }
+
+ function t(key, fallback) {
+ if (window.HyAppI18n && window.HyAppI18n.t) {
+ return window.HyAppI18n.t(key, fallback);
+ }
+ return fallback || key;
+ }
+
+ function formatMessage(key, fallback, values) {
+ var result = t(key, fallback);
+ Object.keys(values || {}).forEach(function (name) {
+ result = result.replace(
+ new RegExp('\\{' + name + '\\}', 'g'),
+ String(values[name])
+ );
+ });
+ return result;
+ }
+
+ function currentLocale() {
+ return window.HyAppI18n && window.HyAppI18n.lang
+ ? window.HyAppI18n.lang()
+ : 'en';
+ }
+
+ function formatNumber(value) {
+ try {
+ return new Intl.NumberFormat(currentLocale()).format(
+ number(value, 0)
+ );
+ } catch (error) {
+ return String(number(value, 0));
+ }
+ }
+
+ function formatScore(value) {
+ // 设计稿的榜单分数是不带千分位的原始整数;不能复用奖励数量的本地化格式,否则会变成 337,700。
+ return String(Math.trunc(number(value, 0)));
+ }
+
+ function pad(value) {
+ return String(value).padStart(2, '0');
+ }
+
+ function shiftedDate(value) {
+ var timestamp = number(value, 0);
+ if (!timestamp) return null;
+ return new Date(timestamp + DISPLAY_OFFSET_MINUTES * 60 * 1000);
+ }
+
+ function formatCycleRange(start, end) {
+ var from = shiftedDate(start);
+ var to = shiftedDate(end);
+ if (!from || !to) return '--.-- --:-----.-- --:--(utc+3)';
+ return (
+ pad(from.getUTCMonth() + 1) +
+ '.' +
+ pad(from.getUTCDate()) +
+ ' ' +
+ pad(from.getUTCHours()) +
+ ':' +
+ pad(from.getUTCMinutes()) +
+ '-' +
+ pad(to.getUTCMonth() + 1) +
+ '.' +
+ pad(to.getUTCDate()) +
+ ' ' +
+ pad(to.getUTCHours()) +
+ ':' +
+ pad(to.getUTCMinutes()) +
+ '(utc+3)'
+ );
+ }
+
+ function formatHistoryRange(start, end) {
+ var from = shiftedDate(start);
+ var to = shiftedDate(end);
+ if (!from || !to) return '----/--/-------/--/--';
+ return (
+ from.getUTCFullYear() +
+ '/' +
+ pad(from.getUTCMonth() + 1) +
+ '/' +
+ pad(from.getUTCDate()) +
+ '-' +
+ to.getUTCFullYear() +
+ '/' +
+ pad(to.getUTCMonth() + 1) +
+ '/' +
+ pad(to.getUTCDate())
+ );
+ }
+
+ function normalizeTab(value) {
+ if (value === 'weekly') return 'weekly';
+ if (value === 'reward' || value === 'rewards') return 'rewards';
+ return 'history';
+ }
+
+ function updateStageSize() {
+ var scale = stageWrap.clientWidth / DESIGN_WIDTH;
+ var height = DESIGN_HEIGHTS[state.tab] || DESIGN_HEIGHTS.history;
+ stageWrap.style.setProperty('--design-scale', String(scale));
+ stageWrap.style.height = Math.ceil(height * scale) + 'px';
+ document.body.style.minHeight = stageWrap.offsetHeight + 'px';
+ }
+
+ function updateURL(tab) {
+ if (!window.history || !window.history.replaceState) return;
+ try {
+ var url = new URL(window.location.href);
+ url.searchParams.set('tab', tab);
+ url.hash = '';
+ window.history.replaceState(null, '', url.toString());
+ } catch (error) {
+ // 受限 WebView 可能禁用 History API;Tab 本身仍可正常切换,不让 URL 同步失败阻断页面。
+ }
+ }
+
+ function setTab(nextTab, syncURL) {
+ var next = normalizeTab(nextTab);
+ state.tab = next;
+ root.setAttribute('data-tab', next);
+ tabs.forEach(function (tab) {
+ var active = tab.getAttribute('data-tab-target') === next;
+ tab.classList.toggle('is-active', active);
+ tab.setAttribute('aria-selected', active ? 'true' : 'false');
+ tab.setAttribute('tabindex', active ? '0' : '-1');
+ });
+ panels.forEach(function (panel) {
+ panel.hidden =
+ panel.id !==
+ 'famiPanel' + next.charAt(0).toUpperCase() + next.slice(1);
+ });
+ updateStageSize();
+ if (syncURL) updateURL(next);
+ }
+
+ function imageURL(source) {
+ var resource = firstObject(source, ['resource']);
+ return text(
+ first(
+ source,
+ [
+ 'image_url',
+ 'imageUrl',
+ 'cover_url',
+ 'coverUrl',
+ 'preview_url',
+ 'previewUrl',
+ 'asset_url',
+ 'assetUrl',
+ 'animation_url',
+ 'animationUrl',
+ 'icon_url',
+ 'iconUrl',
+ 'image',
+ ],
+ first(
+ resource,
+ [
+ 'preview_url',
+ 'previewUrl',
+ 'asset_url',
+ 'assetUrl',
+ 'animation_url',
+ 'animationUrl',
+ 'image_url',
+ 'imageUrl',
+ ],
+ ''
+ )
+ )
+ );
+ }
+
+ function setImage(node, url, alt) {
+ node.alt = text(alt);
+ if (!text(url)) {
+ node.removeAttribute('src');
+ return;
+ }
+ node.src = text(url);
+ node.onerror = function () {
+ // 资源 CDN 失败时移除坏图标,由同一独立卡位内的 CSS 占位层接管,不能回退到设计示例业务媒体。
+ node.removeAttribute('src');
+ };
+ }
+
+ function initial(value) {
+ var normalized = text(value);
+ return normalized ? normalized.charAt(0) : '?';
+ }
+
+ function normalizeEntry(entry, index) {
+ entry = entry || {};
+ var user = firstObject(entry, ['user']);
+ return {
+ rank: number(
+ first(entry, ['rank_no', 'rankNo', 'rank'], index + 1),
+ index + 1
+ ),
+ name: text(
+ first(
+ user,
+ ['username', 'display_user_id', 'displayUserId'],
+ first(entry, ['user_id', 'userId'], '')
+ )
+ ),
+ score: number(first(entry, ['score'], 0), 0),
+ avatar: text(
+ first(user, ['avatar', 'avatar_url', 'avatarUrl'], '')
+ ),
+ };
+ }
+
+ function normalizeEntries(entries) {
+ return (Array.isArray(entries) ? entries : []).map(normalizeEntry);
+ }
+
+ function rankMedal(rank) {
+ if (rank === 1) return LAYER_BASE + 'medal-1.webp';
+ if (rank === 2) return LAYER_BASE + 'medal-2.webp';
+ if (rank === 3) return LAYER_BASE + 'medal-3.webp';
+ return '';
+ }
+
+ function renderListState(target, message) {
+ target.innerHTML = '';
+ var stateNode = document.createElement('div');
+ stateNode.className = 'fami-list-state';
+ stateNode.textContent = message;
+ target.appendChild(stateNode);
+ }
+
+ function renderRankList(
+ target,
+ entries,
+ emptyMessage,
+ errorMessage,
+ loading
+ ) {
+ if (loading) {
+ renderListState(target, t('fami_week_star.loading', 'Loading...'));
+ return;
+ }
+ if (errorMessage) {
+ renderListState(
+ target,
+ t('fami_week_star.load_failed', 'Unable to load rankings.')
+ );
+ return;
+ }
+
+ var rows = normalizeEntries(entries).slice(0, MAX_VISIBLE_RANKS);
+ if (!rows.length) {
+ renderListState(target, emptyMessage);
+ return;
+ }
+
+ target.innerHTML = '';
+ rows.forEach(function (entry) {
+ var fragment = rankTemplate.content.cloneNode(true);
+ var row = fragment.querySelector('.fami-rank-row');
+ var medal = fragment.querySelector('.fami-rank-medal');
+ var rankNumber = fragment.querySelector('.fami-rank-number');
+ var avatar = fragment.querySelector('.fami-rank-avatar img');
+ var avatarPlaceholder = fragment.querySelector(
+ '.fami-avatar-placeholder'
+ );
+ var name = fragment.querySelector('.fami-rank-name');
+ var score = fragment.querySelector('.fami-rank-score');
+ var displayName =
+ entry.name || t('fami_week_star.unknown', 'Unknown');
+
+ setImage(
+ medal,
+ rankMedal(entry.rank),
+ formatMessage('fami_week_star.rank_aria', 'Rank {rank}', {
+ rank: entry.rank,
+ })
+ );
+ rankNumber.textContent = String(entry.rank);
+ setImage(avatar, entry.avatar, displayName);
+ avatarPlaceholder.textContent = initial(displayName);
+ name.textContent = displayName;
+ score.textContent = formatScore(entry.score);
+ row.setAttribute(
+ 'aria-label',
+ formatMessage(
+ 'fami_week_star.entry_aria',
+ 'Rank {rank}, {name}, {score}',
+ {
+ rank: entry.rank,
+ name: displayName,
+ score: formatScore(entry.score),
+ }
+ )
+ );
+ target.appendChild(fragment);
+ });
+ }
+
+ function normalizeGift(gift) {
+ gift = gift || {};
+ var resource = firstObject(gift, ['resource']);
+ return {
+ name: text(
+ first(
+ gift,
+ ['name'],
+ first(resource, ['name'], t('fami_week_star.gift', 'Gift'))
+ )
+ ),
+ image: imageURL(gift),
+ };
+ }
+
+ function renderGifts(gifts, stateMessage) {
+ var normalized = (Array.isArray(gifts) ? gifts : [])
+ .map(normalizeGift)
+ .slice(0, 3);
+ giftListNode.innerHTML = '';
+ giftListNode.classList.toggle('has-state', Boolean(stateMessage));
+
+ if (stateMessage) {
+ var stateNode = document.createElement('div');
+ stateNode.className = 'fami-current-state';
+ stateNode.setAttribute('role', 'status');
+ stateNode.textContent = stateMessage;
+ giftListNode.appendChild(stateNode);
+ return;
+ }
+
+ for (var index = 0; index < 3; index += 1) {
+ var gift = normalized[index] || {
+ name: t('fami_week_star.gift', 'Gift'),
+ image: '',
+ };
+ var fragment = giftTemplate.content.cloneNode(true);
+ var card = fragment.querySelector('.fami-gift-card');
+ var image = fragment.querySelector('.fami-gift-media img');
+ fragment.querySelector('[data-gift-name]').textContent = gift.name;
+ setImage(image, gift.image, gift.name);
+ card.setAttribute('aria-label', gift.name);
+ giftListNode.appendChild(fragment);
+ }
+ }
+
+ function rewardImage(item) {
+ return imageURL(item);
+ }
+
+ function rewardLabel(item, group) {
+ item = item || {};
+ group = group || {};
+ var resource = firstObject(item, ['resource']);
+ var walletType = text(
+ first(item, ['wallet_asset_type', 'walletAssetType'], '')
+ ).toUpperCase();
+ var label = text(
+ first(
+ item,
+ ['label', 'name'],
+ first(resource, ['name'], first(group, ['name'], ''))
+ )
+ );
+ if (!label && walletType === 'COIN') {
+ label = t('fami_week_star.coins', 'Coins');
+ }
+ if (!label) label = t('fami_week_star.reward', 'Reward');
+
+ var durationMS = number(
+ first(item, ['duration_ms', 'durationMs'], 0),
+ 0
+ );
+ var days = Math.round(durationMS / 86400000);
+ if (durationMS > 0 && days > 0) {
+ label = formatMessage(
+ 'fami_week_star.reward_days',
+ '{name} ({days} days)',
+ { name: label, days: days }
+ );
+ }
+
+ var quantity = number(first(item, ['quantity'], 0), 0);
+ var walletAmount = number(
+ first(item, ['wallet_asset_amount', 'walletAssetAmount'], 0),
+ 0
+ );
+ if (quantity > 1) label += ' ×' + formatNumber(quantity);
+ else if (walletAmount > 0) label += ' ×' + formatNumber(walletAmount);
+ return label;
+ }
+
+ function normalizeRewardGroups(rewards) {
+ var source = Array.isArray(rewards) ? rewards : [];
+ return [1, 2, 3].map(function (rank) {
+ var reward = source.find(function (candidate) {
+ return (
+ number(
+ first(candidate, ['rank_no', 'rankNo', 'rank'], 0),
+ 0
+ ) === rank
+ );
+ });
+ reward = reward || {};
+ var group = firstObject(reward, [
+ 'resource_group',
+ 'resourceGroup',
+ 'reward_resource_group',
+ 'rewardResourceGroup',
+ ]);
+ var items = firstArray(reward, ['items', 'resources']);
+ if (!items.length)
+ items = firstArray(group, ['items', 'resources']);
+ return {
+ rank: rank,
+ group: group,
+ items: items.slice(0, 3),
+ };
+ });
+ }
+
+ function renderRewards(rewards, stateMessage) {
+ rewardGroupsNode.innerHTML = '';
+ var groups = normalizeRewardGroups(rewards);
+ if (
+ !stateMessage &&
+ !groups.some(function (groupData) {
+ return groupData.items.length > 0;
+ })
+ ) {
+ stateMessage = t(
+ 'fami_week_star.empty_rewards',
+ 'No reward data yet.'
+ );
+ }
+ if (stateMessage) {
+ var stateNode = document.createElement('div');
+ stateNode.className = 'fami-reward-state';
+ stateNode.setAttribute('role', 'status');
+ stateNode.textContent = stateMessage;
+ rewardGroupsNode.appendChild(stateNode);
+ return;
+ }
+ groups.forEach(function (groupData) {
+ var groupFragment = rewardGroupTemplate.content.cloneNode(true);
+ var groupNode = groupFragment.querySelector('.fami-reward-group');
+ var heading = groupFragment.querySelector(
+ '.fami-reward-heading h2'
+ );
+ var itemsNode = groupFragment.querySelector('.fami-reward-items');
+ heading.textContent = 'TOP' + groupData.rank;
+ itemsNode.setAttribute(
+ 'data-count',
+ String(groupData.items.length)
+ );
+
+ groupData.items.forEach(function (item) {
+ var itemFragment = rewardItemTemplate.content.cloneNode(true);
+ var itemNode = itemFragment.querySelector('.fami-reward-item');
+ var image = itemFragment.querySelector(
+ '.fami-reward-media img'
+ );
+ var label = rewardLabel(item, groupData.group);
+ setImage(image, rewardImage(item), label);
+ itemFragment.querySelector('.fami-reward-label').textContent =
+ label;
+ itemNode.setAttribute('aria-label', label);
+ itemsNode.appendChild(itemFragment);
+ });
+ groupNode.setAttribute(
+ 'aria-label',
+ formatMessage(
+ 'fami_week_star.top_rewards_aria',
+ 'Top {rank} rewards',
+ { rank: groupData.rank }
+ )
+ );
+ rewardGroupsNode.appendChild(groupFragment);
+ });
+ }
+
+ function cycleOf(value) {
+ return firstObject(value, ['cycle']);
+ }
+
+ function render() {
+ var current = state.current || {};
+ var cycle = cycleOf(current);
+ var history = state.historyCycles[0] || {};
+ var historyCycle = cycleOf(history);
+ var currentGifts = firstArray(cycle, ['gifts']);
+ var currentRewards = firstArray(cycle, ['rewards']);
+ var historyEntries = firstArray(history, [
+ 'top_entries',
+ 'topEntries',
+ 'entries',
+ ]);
+ var hasCycle = Boolean(
+ text(first(cycle, ['cycle_id', 'cycleId'], '')) ||
+ number(first(cycle, ['start_ms', 'startMs'], 0), 0)
+ );
+ var currentStateMessage = '';
+ if (state.currentLoading) {
+ currentStateMessage = t('fami_week_star.loading', 'Loading...');
+ } else if (state.currentError) {
+ currentStateMessage = t(
+ 'fami_week_star.activity_load_failed',
+ 'Unable to load Weekly Star.'
+ );
+ } else if (!hasCycle) {
+ currentStateMessage = t(
+ 'fami_week_star.no_active_cycle',
+ 'No active cycle.'
+ );
+ }
+ var rewardStateMessage = currentStateMessage;
+ if (!rewardStateMessage && !currentRewards.length) {
+ rewardStateMessage = t(
+ 'fami_week_star.empty_rewards',
+ 'No reward data yet.'
+ );
+ }
+
+ cycleRangeNode.textContent = formatCycleRange(
+ first(cycle, ['start_ms', 'startMs'], 0),
+ first(cycle, ['end_ms', 'endMs'], 0)
+ );
+ historyRangeNode.textContent = formatHistoryRange(
+ first(historyCycle, ['start_ms', 'startMs'], 0),
+ first(historyCycle, ['end_ms', 'endMs'], 0)
+ );
+ renderGifts(currentGifts, currentStateMessage);
+ renderRankList(
+ weeklyListNode,
+ state.weeklyEntries,
+ t('fami_week_star.empty_weekly', 'No ranking data yet.'),
+ state.currentError,
+ state.weeklyLoading
+ );
+ renderRankList(
+ historyListNode,
+ historyEntries,
+ t('fami_week_star.empty_history', 'No previous ranking records.'),
+ state.historyError,
+ state.historyLoading
+ );
+ renderRewards(currentRewards, rewardStateMessage);
+ }
+
+ function mockData() {
+ var start = Date.UTC(2026, 4, 27, 21, 0, 0);
+ var end = Date.UTC(2026, 5, 5, 20, 59, 0);
+ var weekly = [
+ {
+ rank_no: 1,
+ score: 337700,
+ user: {
+ username: 'hanneem',
+ avatar: FIXTURE_BASE + 'mock-avatar-top.webp',
+ },
+ },
+ {
+ rank_no: 2,
+ score: 337700,
+ user: {
+ username: 'hanneem',
+ avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
+ },
+ },
+ {
+ rank_no: 3,
+ score: 337700,
+ user: {
+ username: 'hanneem',
+ avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
+ },
+ },
+ {
+ rank_no: 4,
+ score: 337700,
+ user: {
+ username: 'hanneem',
+ avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
+ },
+ },
+ {
+ rank_no: 5,
+ score: 337700,
+ user: {
+ username: 'hanneem',
+ avatar: FIXTURE_BASE + 'mock-avatar-user.webp',
+ },
+ },
+ ];
+ var rewardItems = [1, 2, 3].map(function () {
+ return {
+ name: t('fami_week_star.mock_reward_name', 'Frame'),
+ duration_ms: 7 * 86400000,
+ image: FIXTURE_BASE + 'mock-gift.webp',
+ };
+ });
+ return {
+ current: {
+ cycle: {
+ cycle_id: 'mock-fami-week-star',
+ start_ms: start,
+ end_ms: end,
+ gifts: [1, 2, 3].map(function () {
+ return {
+ name: t('fami_week_star.gift', 'Gift'),
+ image: FIXTURE_BASE + 'mock-gift.webp',
+ };
+ }),
+ rewards: [1, 2, 3].map(function (rank) {
+ return {
+ rank_no: rank,
+ resource_group: { items: rewardItems },
+ };
+ }),
+ },
+ top_entries: weekly,
+ server_time_ms: Date.now(),
+ },
+ weekly: weekly,
+ history: [
+ {
+ cycle: {
+ start_ms: Date.UTC(2025, 11, 6, 21, 0, 0),
+ end_ms: Date.UTC(2025, 11, 14, 20, 59, 0),
+ },
+ top_entries: weekly,
+ },
+ ],
+ };
+ }
+
+ function errorText(error) {
+ return text(error && error.message) || 'request_failed';
+ }
+
+ function loadMock() {
+ var mock = mockData();
+ state.currentLoading = false;
+ state.weeklyLoading = false;
+ state.historyLoading = false;
+ state.current = mock.current;
+ state.weeklyEntries = mock.weekly;
+ state.historyCycles = mock.history;
+ state.currentError = '';
+ state.historyError = '';
+ render();
+ }
+
+ function loadReal() {
+ var api = window.HyAppAPI && window.HyAppAPI.weeklyStar;
+ state.currentLoading = true;
+ state.weeklyLoading = true;
+ state.historyLoading = true;
+ state.currentError = '';
+ state.historyError = '';
+ render();
+
+ if (!api) {
+ state.currentLoading = false;
+ state.weeklyLoading = false;
+ state.historyLoading = false;
+ state.currentError = 'api_unavailable';
+ state.historyError = 'api_unavailable';
+ render();
+ return;
+ }
+
+ // history 与 current/leaderboard 使用独立 loading/error 状态;慢榜单不能阻塞已经成功的历史记录,任一接口失败也不清空另一块数据。
+ api.history(10).then(
+ function (data) {
+ state.historyCycles = firstArray(data || {}, ['cycles']);
+ state.historyError = '';
+ state.historyLoading = false;
+ render();
+ },
+ function (error) {
+ state.historyCycles = [];
+ state.historyError = errorText(error);
+ state.historyLoading = false;
+ render();
+ }
+ );
+
+ api.current()
+ .then(function (data) {
+ state.current = data || {};
+ state.currentError = '';
+ state.currentLoading = false;
+ // current 一到就先展示周期、礼物和奖励,不等待只影响本周榜的 leaderboard。
+ render();
+
+ var embedded = firstArray(state.current, [
+ 'top_entries',
+ 'topEntries',
+ ]);
+ var cycle = cycleOf(state.current);
+ var cycleID = text(first(cycle, ['cycle_id', 'cycleId'], ''));
+ if (!cycleID) return embedded;
+
+ // current.top_entries 可直接兜底;leaderboard 额外请求只取设计允许展示的前 5 名。
+ // 排行接口独立失败时保留 current 数据,而不是把整个活动判为不可用。
+ return api
+ .leaderboard({
+ cycle_id: cycleID,
+ page_size: MAX_VISIBLE_RANKS,
+ })
+ .then(function (data) {
+ var entries = firstArray(data || {}, ['entries']);
+ return entries.length ? entries : embedded;
+ })
+ .catch(function () {
+ return embedded;
+ });
+ })
+ .then(function (entries) {
+ state.weeklyEntries = Array.isArray(entries) ? entries : [];
+ state.weeklyLoading = false;
+ render();
+ })
+ .catch(function (error) {
+ // current 失败时周期、礼物、本周榜和奖励统一进入明确错误态;历史请求仍独立完成并保留可用结果。
+ state.current = null;
+ state.currentLoading = false;
+ state.weeklyLoading = false;
+ state.currentError = errorText(error);
+ state.weeklyEntries = [];
+ render();
+ });
+ }
+
+ function bindTabs() {
+ tabs.forEach(function (tab, index) {
+ tab.addEventListener('click', function () {
+ setTab(tab.getAttribute('data-tab-target'), true);
+ });
+ tab.addEventListener('keydown', function (event) {
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
+ return;
+ }
+ event.preventDefault();
+ var direction = event.key === 'ArrowRight' ? 1 : -1;
+ if (document.documentElement.dir === 'rtl') direction *= -1;
+ var nextIndex = (index + direction + tabs.length) % tabs.length;
+ tabs[nextIndex].focus();
+ setTab(tabs[nextIndex].getAttribute('data-tab-target'), true);
+ });
+ });
+ }
+
+ function bindResize() {
+ var scheduled = false;
+ function schedule() {
+ if (scheduled) return;
+ scheduled = true;
+ window.requestAnimationFrame(function () {
+ scheduled = false;
+ updateStageSize();
+ });
+ }
+ window.addEventListener('resize', schedule);
+ window.addEventListener('orientationchange', schedule);
+ if (window.ResizeObserver) {
+ new ResizeObserver(schedule).observe(stageWrap);
+ }
+ }
+
+ function init() {
+ bindTabs();
+ bindResize();
+ setTab(state.tab, false);
+ window.addEventListener('hyapp:i18n-ready', function () {
+ // common/i18n 已更新静态 data-i18n 节点;这里只重算动态 API 文案与数字,避免重复 apply 触发事件循环。
+ render();
+ });
+ if (useMock) loadMock();
+ else loadReal();
+ }
+
+ init();
+})();
diff --git a/activity/week-star/fami/product.js b/activity/week-star/fami/product.js
new file mode 100644
index 0000000..d68ff2d
--- /dev/null
+++ b/activity/week-star/fami/product.js
@@ -0,0 +1,31 @@
+(function () {
+ 'use strict';
+
+ var APP_CODE = 'fami';
+
+ window.HyAppDefaultAppCode = APP_CODE;
+ window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
+ document.documentElement.setAttribute('data-hy-app-code', APP_CODE);
+
+ // 该入口只属于 Fami。common/api.js 的请求租户以 app_code query 为最高优先级,若宿主误带其他
+ // App 参数,仅设置页面默认值仍会串到错误活动周期;在公共脚本初始化前统一 URL,保留 token/env 等参数。
+ try {
+ var url = new URL(window.location.href);
+ var changed = url.searchParams.get('app_code') !== APP_CODE;
+ url.searchParams.set('app_code', APP_CODE);
+ ['app', 'appCode'].forEach(function (key) {
+ if (
+ url.searchParams.has(key) &&
+ url.searchParams.get(key) !== APP_CODE
+ ) {
+ url.searchParams.set(key, APP_CODE);
+ changed = true;
+ }
+ });
+ if (changed && window.history && window.history.replaceState) {
+ window.history.replaceState(null, '', url.toString());
+ }
+ } catch (error) {
+ // 极少数受限 WebView 不允许构造或替换 URL;页面默认 App 仍能覆盖“未传 app_code”的正常入口。
+ }
+})();
diff --git a/activity/week-star/huwaa.html b/activity/week-star/huwaa.html
new file mode 100644
index 0000000..61c70c0
--- /dev/null
+++ b/activity/week-star/huwaa.html
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+ Weekly Star
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+ Weekly Star
+
+
+
+
+

+
+
+
+
+ Note: Send the following three gifts to participate in
+ the ranking. Users who rank in the top 3 of the
+ leaderboard will receive rewards.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/activity/week-star/huwaa/assets/layers/background-stars.webp b/activity/week-star/huwaa/assets/layers/background-stars.webp
new file mode 100644
index 0000000..c0c2ec9
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/background-stars.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/date-ribbon.webp b/activity/week-star/huwaa/assets/layers/date-ribbon.webp
new file mode 100644
index 0000000..48ee57e
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/date-ribbon.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/gift-frame.webp b/activity/week-star/huwaa/assets/layers/gift-frame.webp
new file mode 100644
index 0000000..31bf21d
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/gift-frame.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/hero-background.webp b/activity/week-star/huwaa/assets/layers/hero-background.webp
new file mode 100644
index 0000000..d0a127d
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/hero-background.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/manifest.json b/activity/week-star/huwaa/assets/layers/manifest.json
new file mode 100644
index 0000000..75d089c
--- /dev/null
+++ b/activity/week-star/huwaa/assets/layers/manifest.json
@@ -0,0 +1,503 @@
+{
+ "schemaVersion": 1,
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "canvas": {
+ "id": "543:1998",
+ "name": "Huwaa运营"
+ },
+ "entryFrames": {
+ "history": {
+ "id": "818:1999",
+ "name": "历史前三",
+ "width": 1080,
+ "height": 2309
+ },
+ "weekly": {
+ "id": "818:2066",
+ "name": "本周榜单",
+ "width": 1080,
+ "height": 2309
+ },
+ "rewards": {
+ "id": "818:2132",
+ "name": "奖励说明",
+ "width": 1080,
+ "height": 3138
+ }
+ }
+ },
+ "policy": {
+ "rule": "Only independently traced visual layers may ship. Localized text, controls, state, lists, and production business media stay in DOM/CSS/API data.",
+ "forbiddenSourceNodes": [
+ "543:1998",
+ "818:1999",
+ "818:2066",
+ "818:2132"
+ ],
+ "forbiddenFilenamePatterns": [
+ "whole-page",
+ "full-page",
+ "page-screenshot",
+ "screen-shot",
+ "root-frame",
+ "entry-frame",
+ "figma-frame",
+ "aggregate"
+ ],
+ "forbiddenRuntimeReferences": [
+ ".audit/",
+ "hero-source.webp",
+ "hero-original.webp",
+ "hero-with-text.webp",
+ "hero-exact-fill.webp"
+ ],
+ "maxReasonableFileLayer": {
+ "width": 1800,
+ "height": 2400,
+ "pixels": 3200000
+ }
+ },
+ "assets": [
+ {
+ "filename": "background-stars.webp",
+ "scope": "runtime",
+ "kind": "single-background-image-fill",
+ "sourceNode": "818:2003",
+ "sourceNodes": ["818:2003", "818:2070", "818:2136"],
+ "sourceName": "已生成图片 3",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "ff72a2aa619bf8f79c544ae3e976a015e9b91583",
+ "use": "Pure star-palace background layer behind DOM content",
+ "renderedDesignSize": {
+ "width": 1300,
+ "height": 2309
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 941,
+ "pixelHeight": 1672,
+ "bytes": 151446
+ },
+ "sha256": "ae690d230d418f234cf51b5645dacfe892052eca91a3b41097ee1e8102963f36",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "hero-background.webp",
+ "scope": "runtime",
+ "kind": "single-derived-background",
+ "sourceNode": "818:2006",
+ "sourceNodes": ["818:2006", "818:2073", "818:2139"],
+ "sourceName": "图层 1 5",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "cbc4013cd84d895e3a3ebf260e42b1013066c6ae",
+ "use": "Decorative hero background only; title and date are live DOM/i18n/API content",
+ "renderedDesignSize": {
+ "width": 1080,
+ "height": 1080
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 1254,
+ "pixelHeight": 1254,
+ "bytes": 553084
+ },
+ "sha256": "143b0ceee3c3c484b47f11c87c35d3985eea04aac2d784ac3df154dece5353b6",
+ "derivedFrom": {
+ "kind": "exact-figma-image-fill",
+ "sourceNodes": ["818:2006", "818:2073", "818:2139"],
+ "sourceImageHash": "cbc4013cd84d895e3a3ebf260e42b1013066c6ae",
+ "tool": "built-in image_gen",
+ "operation": "Removed only the embedded readable title/date text from the exact image fill; retained the decorative background artwork and empty ornamental plaques",
+ "sourceContainedReadableText": true
+ },
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "date-ribbon.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-label-skin",
+ "sourceNode": "818:2080",
+ "sourceNodes": ["818:2080", "818:2140"],
+ "sourceName": "长 2",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "cb0526584cbce4322618c38e8c3aa2ca0acba3bf",
+ "use": "Empty date ribbon behind the live activity period",
+ "renderedDesignSize": {
+ "width": 672,
+ "height": 95
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 672,
+ "pixelHeight": 95,
+ "bytes": 8882
+ },
+ "sha256": "ca001aab298b3d86c5ea23ff928076a2955f45c6b586fa275a959650597c0964",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "gift-frame.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-frame",
+ "sourceNode": "818:2011",
+ "sourceNodes": [
+ "818:2011",
+ "818:2012",
+ "818:2016",
+ "818:2017",
+ "818:2021",
+ "818:2022",
+ "818:2085",
+ "818:2086",
+ "818:2090",
+ "818:2091",
+ "818:2095",
+ "818:2096",
+ "818:2145",
+ "818:2146",
+ "818:2150",
+ "818:2151",
+ "818:2155",
+ "818:2156"
+ ],
+ "sourceName": "图层 0 24",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "2a93765147074c2df60a94c592dc153987231c1d",
+ "use": "Decorative gift frame around live API gift media",
+ "renderedDesignSize": {
+ "width": 257,
+ "height": 257
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 257,
+ "pixelHeight": 257,
+ "bytes": 15066
+ },
+ "sha256": "37180db49b3d8d7c0d1ca71ec3df7c64df708a5c6f576577e1c0c2978f3c3dcc",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "sample-gift.webp",
+ "scope": "fixture",
+ "kind": "single-dynamic-media-fixture",
+ "sourceNode": "818:2013",
+ "sourceNodes": [
+ "818:2013",
+ "818:2018",
+ "818:2023",
+ "818:2087",
+ "818:2092",
+ "818:2097",
+ "818:2147",
+ "818:2152",
+ "818:2157",
+ "818:2181",
+ "818:2186",
+ "818:2191",
+ "818:2203",
+ "818:2208",
+ "818:2213",
+ "818:2225",
+ "818:2230",
+ "818:2235"
+ ],
+ "sourceName": "10",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "b046fadadba4b9ef6f75b336460388fe074f48e9",
+ "use": "Figma sample gift used only by the explicit ?mock=1 visual fixture; production media comes from the API",
+ "renderedDesignSize": {
+ "width": 188,
+ "height": 188
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 144,
+ "pixelHeight": 144,
+ "bytes": 15186
+ },
+ "sha256": "55e86aec9054813a6d6277cc9c9b69cf0e39a1303caf2c90255e67c21c8cf1c6",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": true,
+ "mockOnly": true
+ },
+ {
+ "filename": "tab-active.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-tab-skin",
+ "sourceNode": "818:2024",
+ "sourceNodes": ["818:2024", "818:2098", "818:2165"],
+ "sourceName": "图层 0 25",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "f01e6d52e89d3e454c92c63eebf799237e92897b",
+ "use": "Active tab skin under a real DOM button and localized label",
+ "renderedDesignSize": {
+ "width": 335,
+ "height": 149
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 335,
+ "pixelHeight": 149,
+ "bytes": 13332
+ },
+ "sha256": "6438c7dbb4fbb1763201645f9a154d19a240ca78ac1ff30934c4517cf58b909d",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "tab-inactive.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-tab-skin",
+ "sourceNode": "818:2025",
+ "sourceNodes": [
+ "818:2025",
+ "818:2026",
+ "818:2099",
+ "818:2100",
+ "818:2159",
+ "818:2162"
+ ],
+ "sourceName": "阿斯顿撒大 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "327f41c4ed551df8dd3075beb57a2191b68ded43",
+ "use": "Inactive tab skin under a real DOM button and localized label",
+ "renderedDesignSize": {
+ "width": 335,
+ "height": 146
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 335,
+ "pixelHeight": 146,
+ "bytes": 10908
+ },
+ "sha256": "ddfb87104d838ac72b3167614edb34af7f260f16a9b6a111e3a32b1b7b576440",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "rank-1.webp",
+ "scope": "runtime",
+ "kind": "single-static-rank-icon",
+ "sourceNode": "818:2041",
+ "sourceNodes": ["818:2041", "818:2107"],
+ "sourceName": "图层 2 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "81c55bce70ac66a5f8aada3131aec4b8de553fe4",
+ "use": "Fixed rank-one trophy icon beside live leaderboard identity and score data",
+ "renderedDesignSize": {
+ "width": 69,
+ "height": 61
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 69,
+ "pixelHeight": 61,
+ "bytes": 2896
+ },
+ "sha256": "5e1ec9de4a8b3b1da05382f668400e3f69bb842089d550d379b7b1d84d3fcbf0",
+ "containsText": false,
+ "containsFixedOrdinalGlyph": true,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "rank-2.webp",
+ "scope": "runtime",
+ "kind": "single-static-rank-icon",
+ "sourceNode": "818:2046",
+ "sourceNodes": ["818:2046", "818:2112"],
+ "sourceName": "图层 2 2",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "d65314646fb6a249672b27987cf8c0dd4bc42b4e",
+ "use": "Fixed rank-two trophy icon beside live leaderboard identity and score data",
+ "renderedDesignSize": {
+ "width": 69,
+ "height": 61
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 69,
+ "pixelHeight": 61,
+ "bytes": 2630
+ },
+ "sha256": "6919bc82a2449b17a985b668365bf251d5984b4a3af1624d0fd4b8974fff1ec8",
+ "containsText": false,
+ "containsFixedOrdinalGlyph": true,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "rank-3.webp",
+ "scope": "runtime",
+ "kind": "single-static-rank-icon",
+ "sourceNode": "818:2053",
+ "sourceNodes": ["818:2053", "818:2119"],
+ "sourceName": "图层 4 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "5e47a256c75d5b5a54e7b59e5338bd1dff47f596",
+ "use": "Fixed rank-three trophy icon beside live leaderboard identity and score data",
+ "renderedDesignSize": {
+ "width": 63,
+ "height": 61
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 63,
+ "pixelHeight": 61,
+ "bytes": 2606
+ },
+ "sha256": "af998a71318314016ec8ebb8aba15e59224cb0c054b0390eabcfd51e6eb8ed71",
+ "containsText": false,
+ "containsFixedOrdinalGlyph": true,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "reward-panel.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-frame",
+ "sourceNode": "818:2168",
+ "sourceNodes": ["818:2168"],
+ "sourceName": "中小城镇 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "6b120df5508b85ec04f80772bc0a92fa75d5ced3",
+ "use": "Empty reward-section border around DOM headings and reward rows",
+ "renderedDesignSize": {
+ "width": 1033,
+ "height": 1669
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 1033,
+ "pixelHeight": 1669,
+ "bytes": 73126
+ },
+ "sha256": "18f31785c7656b76942ef435f7fd2027b62faac357ceafd658e21c2f79fa9ec0",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "reward-heading.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-label-skin",
+ "sourceNode": "818:2170",
+ "sourceNodes": ["818:2170", "818:2193", "818:2215"],
+ "sourceName": "勋章呈现出 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "11ba387b94c67d28068e40088dffd4078c26e785",
+ "use": "Empty reward-tier heading behind localized DOM text",
+ "renderedDesignSize": {
+ "width": 626,
+ "height": 98
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 626,
+ "pixelHeight": 98,
+ "bytes": 11140
+ },
+ "sha256": "3d34d3420d76dd1394b1080a0f82d910547aeb9c0dc8209e849412a231c91573",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "reward-item-frame.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-frame",
+ "sourceNode": "818:2179",
+ "sourceNodes": [
+ "818:2179",
+ "818:2184",
+ "818:2189",
+ "818:2201",
+ "818:2206",
+ "818:2211",
+ "818:2223",
+ "818:2228",
+ "818:2233"
+ ],
+ "sourceName": "图层 1 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "652e633725bc472c5892142e3eb4686bfb87a304",
+ "use": "Reward item frame around live reward media and localized DOM labels",
+ "renderedDesignSize": {
+ "width": 257,
+ "height": 270
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 257,
+ "pixelHeight": 270,
+ "bytes": 11644
+ },
+ "sha256": "1ff7fbaf2b14d17fc1eb9135c1136504fa96919d7adea996b115df8447d66b1e",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ },
+ {
+ "filename": "reward-item-glow.webp",
+ "scope": "runtime",
+ "kind": "single-ornamental-overlay",
+ "sourceNode": "818:2180",
+ "sourceNodes": [
+ "818:2180",
+ "818:2185",
+ "818:2190",
+ "818:2202",
+ "818:2207",
+ "818:2212",
+ "818:2224",
+ "818:2229",
+ "818:2234"
+ ],
+ "sourceName": "电商双十一通用弹窗广告_设计页-稿定设计 1",
+ "sourceType": "RECTANGLE",
+ "sourceImageHash": "edd5cb329ba1408850f5ba3dd4fc04519b3ae272",
+ "use": "Decorative glow between each reward frame and live reward media",
+ "renderedDesignSize": {
+ "width": 219,
+ "height": 219
+ },
+ "file": {
+ "format": "webp",
+ "pixelWidth": 219,
+ "pixelHeight": 219,
+ "bytes": 11144
+ },
+ "sha256": "ad79af7942577e5e0a683af447b423c8a35838957c51d4f9b3ab9a31eb02db6a",
+ "containsText": false,
+ "containsControls": false,
+ "containsDynamicBusinessMedia": false,
+ "mockOnly": false
+ }
+ ]
+}
diff --git a/activity/week-star/huwaa/assets/layers/rank-1.webp b/activity/week-star/huwaa/assets/layers/rank-1.webp
new file mode 100644
index 0000000..805db5b
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/rank-1.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/rank-2.webp b/activity/week-star/huwaa/assets/layers/rank-2.webp
new file mode 100644
index 0000000..bfd8da8
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/rank-2.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/rank-3.webp b/activity/week-star/huwaa/assets/layers/rank-3.webp
new file mode 100644
index 0000000..95aae5e
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/rank-3.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/reward-heading.webp b/activity/week-star/huwaa/assets/layers/reward-heading.webp
new file mode 100644
index 0000000..d5f9446
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/reward-heading.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/reward-item-frame.webp b/activity/week-star/huwaa/assets/layers/reward-item-frame.webp
new file mode 100644
index 0000000..f7d3f9b
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/reward-item-frame.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/reward-item-glow.webp b/activity/week-star/huwaa/assets/layers/reward-item-glow.webp
new file mode 100644
index 0000000..2434016
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/reward-item-glow.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/reward-panel.webp b/activity/week-star/huwaa/assets/layers/reward-panel.webp
new file mode 100644
index 0000000..4fa2e74
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/reward-panel.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/sample-gift.webp b/activity/week-star/huwaa/assets/layers/sample-gift.webp
new file mode 100644
index 0000000..bc7b004
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/sample-gift.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/tab-active.webp b/activity/week-star/huwaa/assets/layers/tab-active.webp
new file mode 100644
index 0000000..d7bd6d1
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/tab-active.webp differ
diff --git a/activity/week-star/huwaa/assets/layers/tab-inactive.webp b/activity/week-star/huwaa/assets/layers/tab-inactive.webp
new file mode 100644
index 0000000..b156dba
Binary files /dev/null and b/activity/week-star/huwaa/assets/layers/tab-inactive.webp differ
diff --git a/activity/week-star/huwaa/huwaa.css b/activity/week-star/huwaa/huwaa.css
new file mode 100644
index 0000000..e62e4e9
--- /dev/null
+++ b/activity/week-star/huwaa/huwaa.css
@@ -0,0 +1,722 @@
+:root {
+ --huwaa-week-star-width: min(100vw, 430px);
+ --huwaa-week-star-scale: 0.3472222222;
+ /* These page-local colors are measured from Figma nodes 818:1999/2066/2132. */
+ --huwaa-week-star-bg: #1c0536;
+ --huwaa-week-star-row: #1b0a41;
+ --huwaa-week-star-row-alt: rgba(103, 0, 203, 0.2);
+ --huwaa-week-star-text: #f8e8ff;
+ --huwaa-week-star-history: #b856ff;
+}
+
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+html,
+body {
+ width: 100%;
+ min-height: 100%;
+ margin: 0;
+ overflow-x: hidden;
+ background: #0b0219;
+ color: var(--huwaa-week-star-text);
+ font-family:
+ 'Source Han Sans SC',
+ 'Noto Sans CJK SC',
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Arial,
+ sans-serif;
+}
+
+button {
+ margin: 0;
+ border: 0;
+ padding: 0;
+ color: inherit;
+ font: inherit;
+ background: transparent;
+}
+
+button:focus-visible {
+ outline: 6px solid var(--hy-theme-primary, #cf6fff);
+ outline-offset: -8px;
+}
+
+img {
+ display: block;
+ max-width: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.week-star-page {
+ position: relative;
+ width: var(--huwaa-week-star-width);
+ min-height: 0;
+ margin: 0 auto;
+ overflow: hidden;
+ background: var(--huwaa-week-star-bg);
+ direction: ltr;
+}
+
+.week-star-stage-wrap {
+ position: relative;
+ width: var(--huwaa-week-star-width);
+ height: auto;
+ aspect-ratio: 1080 / 2309;
+ overflow: hidden;
+}
+
+.week-star-stage {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 2309px;
+ overflow: hidden;
+ background: var(--huwaa-week-star-bg);
+ transform: scale(var(--huwaa-week-star-scale));
+ transform-origin: top left;
+}
+
+.week-star-background {
+ position: absolute;
+ top: 0;
+ left: -110px;
+ width: 1300px;
+ height: 2309px;
+ opacity: 0.06;
+ object-fit: cover;
+ pointer-events: none;
+}
+
+.week-star-hero,
+.week-star-hero-mask {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1080px;
+ height: 1022px;
+ pointer-events: none;
+}
+
+.week-star-hero-mask {
+ overflow: hidden;
+ -webkit-mask-image: linear-gradient(
+ to bottom,
+ #000 0%,
+ #000 65%,
+ transparent 100%
+ );
+ mask-image: linear-gradient(to bottom, #000 0%, #000 65%, transparent 100%);
+}
+
+.week-star-hero-image {
+ position: absolute;
+ top: -24px;
+ left: 0;
+ width: 1080px;
+ height: 1080px;
+ object-fit: cover;
+}
+
+.week-star-title {
+ position: absolute;
+ top: 622px;
+ left: 145px;
+ width: 790px;
+ height: 126px;
+ margin: 0;
+ overflow: hidden;
+ background: linear-gradient(
+ 180deg,
+ #fff9d0 0%,
+ #f8cf7d 36%,
+ #fff3c4 52%,
+ #b97828 100%
+ );
+ background-clip: text;
+ -webkit-background-clip: text;
+ color: transparent;
+ font-family: Georgia, 'Times New Roman', serif;
+ font-size: 102px;
+ font-weight: 700;
+ line-height: 126px;
+ text-align: center;
+ text-shadow:
+ 0 4px 0 rgba(67, 20, 7, 0.66),
+ 0 9px 14px rgba(0, 0, 0, 0.72);
+ white-space: nowrap;
+}
+
+.week-star-date {
+ position: absolute;
+ top: 778px;
+ left: 204px;
+ width: 672px;
+ height: 95px;
+}
+
+.week-star-page:not([data-tab='history']) .week-star-date {
+ top: 726px;
+}
+
+.week-star-date-ribbon {
+ position: absolute;
+ inset: 0;
+ width: 672px;
+ height: 95px;
+ object-fit: fill;
+}
+
+.week-star-page[data-tab='history'] .week-star-date-ribbon {
+ visibility: hidden;
+}
+
+.week-star-date-text {
+ position: absolute;
+ top: 19px;
+ left: 0;
+ width: 672px;
+ height: 42px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 30px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 30px;
+ text-align: center;
+ text-shadow:
+ 0 2px 2px rgba(31, 0, 72, 0.8),
+ 0 0 10px rgba(255, 120, 255, 0.55);
+ white-space: nowrap;
+}
+
+.week-star-note {
+ position: absolute;
+ top: 861px;
+ left: 64px;
+ width: 952px;
+ height: 126px;
+ margin: 0;
+ overflow: hidden;
+ color: var(--huwaa-week-star-text);
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 42px;
+ text-align: left;
+ text-shadow: 0 2px 6px rgba(0, 0, 0, 0.36);
+}
+
+.week-star-gifts {
+ position: absolute;
+ top: 1027px;
+ left: 0;
+ width: 1080px;
+ height: 257px;
+}
+
+.week-star-gift {
+ position: absolute;
+ top: 0;
+ left: var(--gift-left);
+ width: 257px;
+ height: 257px;
+ overflow: hidden;
+}
+
+.week-star-gift-frame {
+ position: absolute;
+ inset: 0;
+ width: 257px;
+ height: 257px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.week-star-gift-frame-glow {
+ mix-blend-mode: screen;
+}
+
+.week-star-gift-image,
+.week-star-gift-placeholder {
+ position: absolute;
+ top: 41px;
+ left: 35px;
+ width: 188px;
+ height: 188px;
+}
+
+.week-star-gift-image {
+ object-fit: contain;
+}
+
+.week-star-gift-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: rgba(255, 255, 255, 0.62);
+ font-size: 62px;
+ font-weight: 700;
+}
+
+.week-star-gift.has-image .week-star-gift-placeholder {
+ display: none;
+}
+
+.week-star-tabs {
+ position: absolute;
+ z-index: 10;
+ top: 1316px;
+ left: 21px;
+ display: grid;
+ grid-template-columns: repeat(3, 335px);
+ column-gap: 18px;
+ width: 1041px;
+ height: 153px;
+}
+
+.week-star-tab {
+ position: relative;
+ width: 335px;
+ height: 153px;
+ cursor: pointer;
+}
+
+.week-star-tab-skin {
+ position: absolute;
+ left: 0;
+ width: 335px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.week-star-tab-skin-active {
+ top: 4px;
+ height: 149px;
+ visibility: hidden;
+}
+
+.week-star-tab-skin-inactive {
+ top: 6px;
+ height: 146px;
+}
+
+.week-star-tab.is-active .week-star-tab-skin-active {
+ visibility: visible;
+}
+
+.week-star-tab.is-active .week-star-tab-skin-inactive {
+ visibility: hidden;
+}
+
+.week-star-tab span {
+ position: absolute;
+ top: 57px;
+ left: 32px;
+ display: flex;
+ width: 270px;
+ height: 77px;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 34px;
+ text-align: center;
+ text-shadow: 0 2px 5px rgba(0, 0, 0, 0.42);
+}
+
+.week-star-tab.is-active span {
+ color: #fff;
+}
+
+.week-star-panel {
+ position: absolute;
+ inset: 0;
+ width: 1080px;
+}
+
+.week-star-history-date {
+ position: absolute;
+ top: 1514px;
+ left: 291px;
+ width: 508px;
+ height: 46px;
+ margin: 0;
+ overflow: hidden;
+ color: var(--huwaa-week-star-history);
+ font-size: 46px;
+ font-weight: 500;
+ line-height: 46px;
+ text-align: center;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ white-space: nowrap;
+}
+
+.week-star-history-panel::before {
+ position: absolute;
+ top: 1467px;
+ left: 0;
+ width: 1080px;
+ height: 140px;
+ background: var(--huwaa-week-star-row);
+ content: '';
+}
+
+.week-star-rank-row {
+ position: absolute;
+ left: 0;
+ width: 1080px;
+ height: 140px;
+ overflow: hidden;
+ color: #fff;
+ text-align: left;
+ cursor: default;
+}
+
+.week-star-rank-row[data-user-id] {
+ cursor: pointer;
+}
+
+.week-star-rank-row.is-dark {
+ background: var(--huwaa-week-star-row);
+}
+
+.week-star-rank-row.is-purple {
+ background: var(--huwaa-week-star-row-alt);
+}
+
+.week-star-rank-medal,
+.week-star-rank-number {
+ position: absolute;
+ top: 39px;
+ left: 45px;
+ width: 69px;
+ height: 61px;
+}
+
+.week-star-rank-medal {
+ object-fit: contain;
+}
+
+.week-star-rank-medal.is-rank-three {
+ left: 47px;
+ width: 63px;
+}
+
+.week-star-rank-number {
+ top: 49px;
+ left: 69px;
+ width: 45px;
+ height: 42px;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 42px;
+}
+
+.week-star-rank-avatar {
+ position: absolute;
+ top: 13px;
+ left: 160px;
+ width: 113px;
+ height: 113px;
+ overflow: hidden;
+ border: 3px solid rgba(255, 255, 255, 0.22);
+ border-radius: 50%;
+ background: rgba(184, 86, 255, 0.3);
+}
+
+.week-star-rank-avatar img {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.week-star-rank-avatar span {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 42px;
+ font-weight: 700;
+ line-height: 1;
+ text-transform: uppercase;
+}
+
+.week-star-rank-avatar.is-fallback img {
+ display: none;
+}
+
+.week-star-rank-name,
+.week-star-rank-score {
+ position: absolute;
+ top: 49px;
+ height: 42px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 42px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.week-star-rank-name {
+ left: 313px;
+ width: 420px;
+}
+
+.week-star-rank-score {
+ left: 846px;
+ width: 190px;
+ text-align: left;
+}
+
+.week-star-state {
+ position: absolute;
+ left: 70px;
+ display: flex;
+ width: 940px;
+ min-height: 140px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 26px;
+ padding: 20px 40px;
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 34px;
+ font-weight: 500;
+ line-height: 46px;
+ text-align: center;
+ text-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
+}
+
+.week-star-state-retry {
+ min-width: 260px;
+ min-height: 76px;
+ border: 3px solid rgba(255, 255, 255, 0.38);
+ border-radius: 38px;
+ padding: 12px 38px;
+ background: var(--hy-theme-button, #be73ed);
+ color: #fff;
+ font-size: 30px;
+ font-weight: 700;
+ line-height: 42px;
+ cursor: pointer;
+}
+
+.week-star-reward-panel-skin {
+ position: absolute;
+ top: 1531px;
+ left: 24px;
+ width: 1033px;
+ height: 1669px;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.week-star-reward-group {
+ position: absolute;
+ left: 91px;
+ width: 900px;
+ height: 424px;
+}
+
+.week-star-reward-heading {
+ position: absolute;
+ top: 0;
+ left: 136px;
+ width: 626px;
+ height: 98px;
+ margin: 0;
+}
+
+.week-star-reward-heading img {
+ position: absolute;
+ inset: 0;
+ width: 626px;
+ height: 98px;
+ object-fit: fill;
+}
+
+.week-star-reward-heading span {
+ position: absolute;
+ top: 43px;
+ left: 0;
+ width: 626px;
+ height: 40px;
+ overflow: hidden;
+ color: #fff;
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'SF Pro Display', Arial, sans-serif;
+ font-size: 40px;
+ font-weight: 500;
+ line-height: 40px;
+ text-align: center;
+ text-shadow:
+ 0 2px 4px rgba(0, 0, 0, 0.45),
+ 0 0 9px rgba(255, 93, 255, 0.55);
+}
+
+.week-star-reward-items {
+ position: absolute;
+ top: 200px;
+ left: 0;
+ display: flex;
+ width: 900px;
+ height: 326px;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 65px;
+}
+
+.week-star-reward-item {
+ position: relative;
+ width: 257px;
+ height: 326px;
+ flex: 0 0 257px;
+}
+
+.week-star-reward-item-frame {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 257px;
+ height: 270px;
+ object-fit: fill;
+}
+
+.week-star-reward-item-glow {
+ position: absolute;
+ top: 30px;
+ left: 19px;
+ width: 219px;
+ height: 219px;
+ opacity: 0.34;
+ object-fit: contain;
+}
+
+.week-star-reward-item-image,
+.week-star-reward-item-placeholder {
+ position: absolute;
+ top: 41px;
+ left: 35px;
+ width: 188px;
+ height: 188px;
+}
+
+.week-star-reward-item-image {
+ object-fit: contain;
+}
+
+.week-star-reward-item-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 58px;
+ font-weight: 700;
+}
+
+.week-star-reward-item.has-image .week-star-reward-item-placeholder {
+ display: none;
+}
+
+.week-star-reward-label {
+ position: absolute;
+ top: 290px;
+ left: 3px;
+ width: 251px;
+ height: 36px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 36px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.week-star-reward-empty {
+ position: absolute;
+ top: 1705px;
+ left: 90px;
+}
+
+.week-star-reward-inline-empty {
+ display: flex;
+ width: 900px;
+ height: 140px;
+ align-items: center;
+ justify-content: center;
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 34px;
+ font-weight: 500;
+ line-height: 46px;
+ text-align: center;
+}
+
+.week-star-announcer {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+}
+
+html[dir='rtl'] .week-star-note {
+ direction: rtl;
+ text-align: right;
+}
+
+html[dir='rtl'] .week-star-tab span,
+html[dir='rtl'] .week-star-state,
+html[dir='rtl'] .week-star-reward-label {
+ direction: rtl;
+}
+
+html[lang='ar'] .week-star-title,
+html[lang='tr'] .week-star-title,
+html[lang='es'] .week-star-title {
+ font-family: 'Source Han Sans SC', Arial, sans-serif;
+ font-size: 82px;
+}
+
+html[lang='tr'] .week-star-tab span,
+html[lang='es'] .week-star-tab span {
+ font-size: 28px;
+ line-height: 31px;
+}
+
+@media (min-width: 1080px) {
+ :root {
+ --huwaa-week-star-width: 430px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition: none !important;
+ }
+}
diff --git a/activity/week-star/huwaa/huwaa.js b/activity/week-star/huwaa/huwaa.js
new file mode 100644
index 0000000..63b5513
--- /dev/null
+++ b/activity/week-star/huwaa/huwaa.js
@@ -0,0 +1,1196 @@
+(function () {
+ 'use strict';
+
+ var ASSET_BASE = './huwaa/assets/layers/';
+ var RANK_LIMIT = 5;
+ var DAY_MS = 24 * 60 * 60 * 1000;
+ var DESIGN_HEIGHT = { history: 2309, weekly: 2309, rewards: 3200 };
+ var DESIGN_FRAME = {
+ history: '818:1999',
+ weekly: '818:2066',
+ rewards: '818:2132',
+ };
+ var params = currentParams();
+ var useMock = params.get('mock') === '1';
+ var page = document.getElementById('weekStarPage');
+ var stage = document.getElementById('weekStarStage');
+ var stageWrap = document.getElementById('weekStarStageWrap');
+ var tabs = all('[data-tab-target]');
+ var panels = all('[data-panel]');
+ var state = {
+ // Once the user switches sections, the hash is the canonical tab state. It
+ // must win over a stale entry `?tab=` value while the hash-query token stays intact.
+ tab: normalizeTab(tabFromHash() || params.get('tab')),
+ current: null,
+ weeklyEntries: [],
+ historyCycles: [],
+ loadingCurrent: true,
+ loadingWeekly: true,
+ loadingHistory: true,
+ currentError: '',
+ weeklyError: '',
+ historyError: '',
+ requestSerial: 0,
+ };
+
+ function all(selector, root) {
+ return Array.prototype.slice.call(
+ (root || document).querySelectorAll(selector)
+ );
+ }
+
+ function currentParams() {
+ if (
+ window.HyAppParams &&
+ window.HyAppParams.current &&
+ window.HyAppParams.current.raw
+ ) {
+ return window.HyAppParams.current.raw;
+ }
+ var result = new URLSearchParams(window.location.search || '');
+ var hash = window.location.hash || '';
+ if (hash.indexOf('?') >= 0) {
+ new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
+ function (value, key) {
+ if (!result.has(key)) result.set(key, value);
+ }
+ );
+ }
+ return result;
+ }
+
+ function text(value) {
+ return String(
+ value === undefined || value === null ? '' : value
+ ).trim();
+ }
+
+ function number(value, fallback) {
+ var result = Number(value);
+ return Number.isFinite(result) ? result : Number(fallback || 0);
+ }
+
+ function first(source, keys) {
+ source = source || {};
+ for (var index = 0; index < keys.length; index += 1) {
+ var value = source[keys[index]];
+ if (value !== undefined && value !== null && value !== '') {
+ return value;
+ }
+ }
+ return '';
+ }
+
+ function envelope(payload) {
+ if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) {
+ return payload.data || {};
+ }
+ return payload || {};
+ }
+
+ function parseJSON(value) {
+ if (!value || typeof value !== 'string') return {};
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ return {};
+ }
+ }
+
+ function t(key, fallback, values) {
+ var translated =
+ window.HyAppI18n && window.HyAppI18n.t
+ ? window.HyAppI18n.t(key, fallback)
+ : fallback || key;
+ Object.keys(values || {}).forEach(function (name) {
+ translated = translated.replace(
+ new RegExp('\\{' + name + '\\}', 'g'),
+ String(values[name])
+ );
+ });
+ return translated;
+ }
+
+ function escapeHTML(value) {
+ return String(value === undefined || value === null ? '' : value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ function escapeAttr(value) {
+ return escapeHTML(value);
+ }
+
+ function resolveAssetURL(value) {
+ var raw = text(value);
+ if (!raw) return '';
+ if (
+ /^(https?:)?\/\//i.test(raw) ||
+ /^(data|blob):/i.test(raw) ||
+ raw.charAt(0) === '/' ||
+ raw.indexOf('./') === 0 ||
+ raw.indexOf('../') === 0
+ ) {
+ return raw;
+ }
+ // Backend resource records may return a path relative to the API host. Resolving
+ // against the API base avoids silently requesting that path from this H5 directory.
+ try {
+ var base =
+ window.HyAppAPI && window.HyAppAPI.baseURL
+ ? window.HyAppAPI.baseURL()
+ : window.location.origin + '/';
+ return new URL(raw, base).toString();
+ } catch (error) {
+ return '';
+ }
+ }
+
+ function formatNumber(value) {
+ var lang =
+ window.HyAppI18n && window.HyAppI18n.lang
+ ? window.HyAppI18n.lang()
+ : 'en';
+ return new Intl.NumberFormat(lang).format(number(value));
+ }
+
+ function formatScore(value) {
+ var raw = text(value);
+ if (!raw) return '0';
+ var numeric = Number(raw);
+ return Number.isFinite(numeric) ? String(numeric) : raw;
+ }
+
+ function pad(value) {
+ return String(value).padStart(2, '0');
+ }
+
+ function timezoneOffsetHours(cycle) {
+ var explicit = first(cycle, [
+ 'utc_offset_hours',
+ 'utcOffsetHours',
+ 'timezone_offset_hours',
+ 'timezoneOffsetHours',
+ ]);
+ if (explicit !== '') return number(explicit, 3);
+ var label = text(first(cycle, ['timezone', 'time_zone', 'timeZone']));
+ var match = label.match(/(?:utc|gmt)\s*([+-]\d+(?:\.\d+)?)/i);
+ // The Figma campaign explicitly uses UTC+3. The API can override this through
+ // its cycle metadata; the design offset is only the fallback display contract.
+ return match ? number(match[1], 3) : 3;
+ }
+
+ function shiftedDate(value, offsetHours) {
+ var timestamp = number(value);
+ if (!timestamp) return null;
+ return new Date(timestamp + offsetHours * 60 * 60 * 1000);
+ }
+
+ function formatDateTime(value, offsetHours) {
+ var date = shiftedDate(value, offsetHours);
+ if (!date) return '—';
+ return [
+ date.getUTCFullYear(),
+ '-',
+ pad(date.getUTCMonth() + 1),
+ '-',
+ pad(date.getUTCDate()),
+ ' ',
+ pad(date.getUTCHours()),
+ ':',
+ pad(date.getUTCMinutes()),
+ ].join('');
+ }
+
+ function formatDate(value, offsetHours) {
+ var date = shiftedDate(value, offsetHours);
+ if (!date) return '—';
+ return [
+ date.getUTCFullYear(),
+ '/',
+ pad(date.getUTCMonth() + 1),
+ '/',
+ pad(date.getUTCDate()),
+ ].join('');
+ }
+
+ function formatOffset(offsetHours) {
+ return (offsetHours >= 0 ? '+' : '') + String(offsetHours);
+ }
+
+ function cycleRange(cycle) {
+ cycle = cycle || {};
+ var custom = text(
+ first(cycle, [
+ 'display_time',
+ 'displayTime',
+ 'time_range',
+ 'timeRange',
+ ])
+ );
+ if (custom) return custom;
+ var offset = timezoneOffsetHours(cycle);
+ return (
+ formatDateTime(first(cycle, ['start_ms', 'startMs']), offset) +
+ '-' +
+ formatDateTime(first(cycle, ['end_ms', 'endMs']), offset) +
+ '(utc' +
+ formatOffset(offset) +
+ ')'
+ );
+ }
+
+ function historyRange(cycle) {
+ cycle = cycle || {};
+ var offset = timezoneOffsetHours(cycle);
+ return (
+ formatDate(first(cycle, ['start_ms', 'startMs']), offset) +
+ '-' +
+ formatDate(first(cycle, ['end_ms', 'endMs']), offset)
+ );
+ }
+
+ function currentCycle() {
+ var data = envelope(state.current);
+ return data.cycle || data.current_cycle || data.currentCycle || null;
+ }
+
+ function errorKey(error) {
+ var status = number(
+ first(error, ['status', 'status_code', 'statusCode'])
+ );
+ var code = text(
+ first(error, ['code', 'error_code', 'errorCode'])
+ ).toUpperCase();
+ var message = text(error && error.message).toLowerCase();
+ if (
+ status === 401 ||
+ code === 'UNAUTHORIZED' ||
+ message.indexOf('unauthor') >= 0
+ ) {
+ return 'huwaa_weekly_star.login_required';
+ }
+ if (code === 'PROFILE_REQUIRED') {
+ return 'huwaa_weekly_star.profile_required';
+ }
+ return 'huwaa_weekly_star.request_failed';
+ }
+
+ function normalizeGift(item) {
+ item = item || {};
+ var resource = item.resource || item.gift || {};
+ var metadata = parseJSON(
+ first(item, ['metadata_json', 'metadataJson']) ||
+ first(resource, ['metadata_json', 'metadataJson'])
+ );
+ return {
+ id: text(
+ first(item, [
+ 'gift_id',
+ 'giftId',
+ 'resource_id',
+ 'resourceId',
+ 'id',
+ ]) || first(resource, ['id', 'resource_id', 'resourceId'])
+ ),
+ name: text(
+ first(item, ['display_name', 'displayName', 'name']) ||
+ first(resource, ['display_name', 'displayName', 'name'])
+ ),
+ image: resolveAssetURL(
+ first(metadata, [
+ 'preview_url',
+ 'previewUrl',
+ 'asset_url',
+ 'assetUrl',
+ ]) ||
+ first(item, [
+ 'image_url',
+ 'imageUrl',
+ 'cover_url',
+ 'coverUrl',
+ 'preview_url',
+ 'previewUrl',
+ 'icon_url',
+ 'iconUrl',
+ 'asset_url',
+ 'assetUrl',
+ ]) ||
+ first(resource, [
+ 'preview_url',
+ 'previewUrl',
+ 'cover_url',
+ 'coverUrl',
+ 'asset_url',
+ 'assetUrl',
+ ])
+ ),
+ };
+ }
+
+ function normalizeEntries(entries) {
+ if (!Array.isArray(entries)) return [];
+ return entries.slice(0, RANK_LIMIT).map(function (entry, index) {
+ entry = entry || {};
+ var user = entry.user || entry.profile || {};
+ var rank = number(
+ first(entry, ['rank_no', 'rankNo', 'rank']),
+ index + 1
+ );
+ var name = text(
+ first(user, [
+ 'username',
+ 'nickname',
+ 'display_name',
+ 'displayName',
+ 'display_user_id',
+ 'displayUserId',
+ ]) || first(entry, ['username', 'name', 'user_id', 'userId'])
+ );
+ return {
+ rank: rank || index + 1,
+ userID: text(
+ first(entry, ['user_id', 'userId']) ||
+ first(user, ['user_id', 'userId', 'id'])
+ ),
+ name: name || t('huwaa_weekly_star.unknown_user', 'User'),
+ score: first(entry, ['score', 'points', 'value']) || 0,
+ avatar: resolveAssetURL(
+ first(user, [
+ 'avatar',
+ 'avatar_url',
+ 'avatarUrl',
+ 'profile_image',
+ 'profileImage',
+ ]) || first(entry, ['avatar', 'avatar_url', 'avatarUrl'])
+ ),
+ };
+ });
+ }
+
+ function rewardImage(item, resource, metadata) {
+ return resolveAssetURL(
+ first(metadata, [
+ 'preview_url',
+ 'previewUrl',
+ 'asset_url',
+ 'assetUrl',
+ ]) ||
+ first(item, [
+ 'image',
+ 'image_url',
+ 'imageUrl',
+ 'cover_url',
+ 'coverUrl',
+ 'preview_url',
+ 'previewUrl',
+ 'icon_url',
+ 'iconUrl',
+ 'asset_url',
+ 'assetUrl',
+ ]) ||
+ first(resource, [
+ 'preview_url',
+ 'previewUrl',
+ 'cover_url',
+ 'coverUrl',
+ 'asset_url',
+ 'assetUrl',
+ 'animation_url',
+ 'animationUrl',
+ ])
+ );
+ }
+
+ function normalizeRewardItem(item, group) {
+ item = item || {};
+ group = group || {};
+ var resource = item.resource || {};
+ var metadata = parseJSON(
+ first(item, ['metadata_json', 'metadataJson']) ||
+ first(resource, ['metadata_json', 'metadataJson'])
+ );
+ var walletType = text(
+ first(item, ['wallet_asset_type', 'walletAssetType']) ||
+ first(resource, ['wallet_asset_type', 'walletAssetType'])
+ ).replace(/_/g, ' ');
+ var walletAmount = number(
+ first(item, [
+ 'wallet_asset_amount',
+ 'walletAssetAmount',
+ 'amount',
+ ]) || first(resource, ['wallet_asset_amount', 'walletAssetAmount'])
+ );
+ var quantity = number(first(item, ['quantity']));
+ var durationMS = number(first(item, ['duration_ms', 'durationMs']));
+ var durationDays = number(
+ first(item, [
+ 'duration_days',
+ 'durationDays',
+ 'valid_days',
+ 'validDays',
+ ]) || first(resource, ['duration_days', 'durationDays'])
+ );
+ if (!durationDays && durationMS > 0) {
+ // Gateway resource-group items expose validity as duration_ms. Keep the
+ // conversion aligned with the backend's whole-day reward contract.
+ durationDays = Math.floor(durationMS / DAY_MS);
+ }
+ var labelKey = text(first(item, ['i18n_key', 'i18nKey']));
+ var label = labelKey
+ ? t(labelKey, '')
+ : text(
+ first(item, [
+ 'display_name',
+ 'displayName',
+ 'label',
+ 'name',
+ ]) ||
+ first(resource, [
+ 'display_name',
+ 'displayName',
+ 'name',
+ ]) ||
+ walletType ||
+ first(group, ['display_name', 'displayName', 'name'])
+ );
+ if (durationDays > 0) {
+ label = t('huwaa_weekly_star.reward_days', '{name} ({days} days)', {
+ name: label || t('huwaa_weekly_star.reward', 'Reward'),
+ days: durationDays,
+ });
+ }
+ if (quantity > 1) {
+ label =
+ (label || t('huwaa_weekly_star.reward', 'Reward')) +
+ ' ×' +
+ formatNumber(quantity);
+ } else if (walletAmount > 0) {
+ label =
+ (label ||
+ walletType ||
+ t('huwaa_weekly_star.reward', 'Reward')) +
+ ' ×' +
+ formatNumber(walletAmount);
+ }
+ return {
+ label: label || t('huwaa_weekly_star.reward', 'Reward'),
+ image: rewardImage(item, resource, metadata),
+ };
+ }
+
+ function normalizeRewardGroups(cycle) {
+ cycle = cycle || {};
+ var source =
+ cycle.rewards ||
+ cycle.reward_groups ||
+ cycle.rewardGroups ||
+ cycle.rank_rewards ||
+ cycle.rankRewards ||
+ [];
+ if (!Array.isArray(source)) return [];
+ return source
+ .map(function (group, index) {
+ group = group || {};
+ var nested =
+ group.resource_group ||
+ group.resourceGroup ||
+ group.reward_resource_group ||
+ group.rewardResourceGroup ||
+ {};
+ var items =
+ group.items ||
+ group.resources ||
+ nested.items ||
+ nested.resources ||
+ [];
+ // Some endpoints flatten a single visual resource onto the rank reward.
+ // It is only promoted when an actual image/name exists; a bare group ID
+ // must remain an empty state rather than a fabricated usable reward.
+ if (!Array.isArray(items)) items = [];
+ if (
+ !items.length &&
+ (rewardImage(group, group.resource || {}, {}) ||
+ first(group, ['display_name', 'displayName', 'name']))
+ ) {
+ items = [group];
+ }
+ return {
+ rank: number(
+ first(group, ['rank_no', 'rankNo', 'rank']),
+ index + 1
+ ),
+ items: items.slice(0, 3).map(function (item) {
+ return normalizeRewardItem(item, group);
+ }),
+ };
+ })
+ .filter(function (group) {
+ return group.rank >= 1 && group.rank <= 3;
+ })
+ .sort(function (left, right) {
+ return left.rank - right.rank;
+ });
+ }
+
+ function renderGifts(cycle) {
+ var source = cycle && Array.isArray(cycle.gifts) ? cycle.gifts : [];
+ var gifts = source.slice(0, 3).map(normalizeGift);
+ var lefts = [61, 411, 761];
+ var root = document.getElementById('weekStarGifts');
+ root.innerHTML = lefts
+ .map(function (left, index) {
+ var gift = gifts[index] || {};
+ var label =
+ gift.name ||
+ t('huwaa_weekly_star.gift_label', 'Activity gift {index}', {
+ index: index + 1,
+ });
+ return [
+ '',
+ '

',
+ '

',
+ gift.image
+ ? '
 +
+ ')
'
+ : '',
+ '
—',
+ '
',
+ ].join('');
+ })
+ .join('');
+ bindImageFallbacks(root);
+ }
+
+ function medalForRank(rank) {
+ if (rank === 1) return ASSET_BASE + 'rank-1.webp';
+ if (rank === 2) return ASSET_BASE + 'rank-2.webp';
+ if (rank === 3) return ASSET_BASE + 'rank-3.webp';
+ return '';
+ }
+
+ function rankInitial(name) {
+ var characters = Array.from(text(name));
+ return characters.length ? characters[0] : '?';
+ }
+
+ function stateHTML(top, message, retry) {
+ return [
+ '',
+ '',
+ escapeHTML(message),
+ '',
+ retry
+ ? ''
+ : '',
+ '
',
+ ].join('');
+ }
+
+ function isRetryable(error) {
+ return error === 'huwaa_weekly_star.request_failed';
+ }
+
+ function renderRankList(options) {
+ var root = document.getElementById(options.rootID);
+ var entries = normalizeEntries(options.entries);
+ if (!entries.length) {
+ var message = options.loading
+ ? t('huwaa_weekly_star.loading', 'Loading…')
+ : options.error
+ ? t(options.error, 'Unable to load the ranking.')
+ : t(options.emptyKey, options.emptyFallback);
+ root.innerHTML = stateHTML(
+ options.top,
+ message,
+ !options.loading && isRetryable(options.error)
+ );
+ return;
+ }
+ root.innerHTML = entries
+ .map(function (entry, index) {
+ var medal = medalForRank(entry.rank);
+ var tag = entry.userID ? 'button' : 'article';
+ var className =
+ index % 2 === (options.firstDark ? 0 : 1)
+ ? 'is-dark'
+ : 'is-purple';
+ var aria = t(
+ 'huwaa_weekly_star.rank_label',
+ 'Rank {rank}, {name}, {score}',
+ {
+ rank: entry.rank,
+ name: entry.name,
+ score: formatScore(entry.score),
+ }
+ );
+ return [
+ '<',
+ tag,
+ tag === 'button' ? ' type="button"' : '',
+ ' class="week-star-rank-row ',
+ className,
+ '" style="top:',
+ options.top + index * 140,
+ 'px"',
+ entry.userID
+ ? ' data-user-id="' + escapeAttr(entry.userID) + '"'
+ : '',
+ ' aria-label="',
+ escapeAttr(aria),
+ '">',
+ medal
+ ? '
'
+ : '' +
+ escapeHTML(entry.rank) +
+ '',
+ '',
+ entry.avatar
+ ? '
'
+ : '',
+ '',
+ escapeHTML(rankInitial(entry.name)),
+ '',
+ '',
+ '',
+ escapeHTML(entry.name),
+ '',
+ '',
+ escapeHTML(formatScore(entry.score)),
+ '',
+ '',
+ tag,
+ '>',
+ ].join('');
+ })
+ .join('');
+ bindImageFallbacks(root);
+ }
+
+ function historyCycleData() {
+ var record = state.historyCycles[0] || {};
+ return {
+ cycle: record.cycle || record,
+ entries:
+ record.top_entries ||
+ record.topEntries ||
+ record.entries ||
+ record.leaderboard ||
+ [],
+ };
+ }
+
+ function renderHistory() {
+ var history = historyCycleData();
+ document.getElementById('weekStarHistoryDate').textContent =
+ history.cycle &&
+ first(history.cycle, ['start_ms', 'startMs']) &&
+ first(history.cycle, ['end_ms', 'endMs'])
+ ? historyRange(history.cycle)
+ : t(
+ 'huwaa_weekly_star.previous_top_three',
+ 'Previous Top 3 Records'
+ );
+ renderRankList({
+ rootID: 'weekStarHistoryList',
+ top: 1607,
+ entries: history.entries,
+ loading: state.loadingHistory,
+ error: state.historyError,
+ emptyKey: 'huwaa_weekly_star.no_history',
+ emptyFallback: 'No previous ranking records.',
+ firstDark: false,
+ });
+ }
+
+ function renderWeekly() {
+ renderRankList({
+ rootID: 'weekStarWeeklyList',
+ top: 1496,
+ entries: state.weeklyEntries,
+ loading: state.loadingWeekly,
+ error: state.weeklyError,
+ emptyKey: 'huwaa_weekly_star.no_ranking',
+ emptyFallback: 'No ranking data yet.',
+ firstDark: true,
+ });
+ }
+
+ function renderRewardItem(item) {
+ return [
+ '',
+ '
',
+ '
',
+ item.image
+ ? '
'
+ : '',
+ '—',
+ '',
+ escapeHTML(item.label),
+ '',
+ '',
+ ].join('');
+ }
+
+ function renderRewards(cycle) {
+ var root = document.getElementById('weekStarRewardGroups');
+ if (state.loadingCurrent) {
+ root.innerHTML = stateHTML(
+ 1705,
+ t('huwaa_weekly_star.loading', 'Loading…'),
+ false
+ );
+ return;
+ }
+ if (state.currentError) {
+ root.innerHTML = stateHTML(
+ 1705,
+ t(state.currentError, 'Unable to load rewards.'),
+ isRetryable(state.currentError)
+ );
+ return;
+ }
+ var groups = normalizeRewardGroups(cycle);
+ if (!groups.length) {
+ root.innerHTML = stateHTML(
+ 1705,
+ t('huwaa_weekly_star.no_rewards', 'No reward data yet.'),
+ false
+ );
+ return;
+ }
+ var positions = {
+ 1: { heading: 1505, items: 1705 },
+ 2: { heading: 2068, items: 2203 },
+ 3: { heading: 2566, items: 2701 },
+ };
+ root.innerHTML = groups
+ .map(function (group) {
+ var position = positions[group.rank];
+ var itemsHTML = group.items.length
+ ? group.items.map(renderRewardItem).join('')
+ : '' +
+ escapeHTML(
+ t(
+ 'huwaa_weekly_star.no_rank_reward',
+ 'No reward data.'
+ )
+ ) +
+ '
';
+ return [
+ '',
+ '',
+ '
',
+ 'TOP',
+ group.rank,
+ '',
+ '
',
+ '',
+ itemsHTML,
+ '
',
+ '',
+ ].join('');
+ })
+ .join('');
+ bindImageFallbacks(root);
+ }
+
+ function renderHeader() {
+ var cycle = currentCycle();
+ var date = document.getElementById('weekStarDate');
+ if (state.loadingCurrent) {
+ date.textContent = '—';
+ } else if (state.currentError) {
+ date.textContent = '—';
+ } else if (cycle) {
+ date.textContent = cycleRange(cycle);
+ } else {
+ date.textContent = t(
+ 'huwaa_weekly_star.no_active_cycle',
+ 'No active cycle'
+ );
+ }
+ renderGifts(cycle || {});
+ renderRewards(cycle || {});
+ }
+
+ function bindImageFallbacks(root) {
+ all('[data-image-fallback]', root).forEach(function (image) {
+ if (image.dataset.fallbackBound === 'true') return;
+ image.dataset.fallbackBound = 'true';
+ function fallback() {
+ var type = image.getAttribute('data-image-fallback');
+ var parent = image.parentElement;
+ if (type === 'avatar' && parent) {
+ parent.classList.add('is-fallback');
+ } else if (parent) {
+ parent.classList.remove('has-image');
+ }
+ image.hidden = true;
+ }
+ image.addEventListener('error', fallback, { once: true });
+ if (image.complete && image.naturalWidth === 0) fallback();
+ });
+ }
+
+ function renderAll() {
+ renderHeader();
+ renderHistory();
+ renderWeekly();
+ setTab(state.tab, false, false);
+ }
+
+ function tabFromHash() {
+ var hash = text((window.location.hash || '').replace(/^#/, ''));
+ if (!hash) return '';
+ var route = hash.split('?')[0].replace(/^\/+|\/+$/g, '');
+ var pieces = route.split('/');
+ return pieces[pieces.length - 1];
+ }
+
+ function normalizeTab(value) {
+ return value === 'weekly' || value === 'rewards' || value === 'history'
+ ? value
+ : 'history';
+ }
+
+ function replaceTabHash(tab) {
+ if (!window.history || !window.history.replaceState) return;
+ var hash = window.location.hash || '';
+ var hashQuery =
+ hash.indexOf('?') >= 0
+ ? '?' + hash.split('?').slice(1).join('?')
+ : '';
+ // Native containers sometimes inject the token after `#?...`; preserving that
+ // suffix is mandatory because replacing the entire hash would log the user out.
+ var next =
+ window.location.pathname +
+ window.location.search +
+ '#' +
+ tab +
+ hashQuery;
+ window.history.replaceState(null, '', next);
+ }
+
+ function updateStageHeight() {
+ var viewportWidth = Math.min(window.innerWidth, 430);
+ var height = DESIGN_HEIGHT[state.tab] || DESIGN_HEIGHT.history;
+ var scaledHeight = Math.ceil((height * viewportWidth) / 1080);
+ document.documentElement.style.setProperty(
+ '--huwaa-week-star-scale',
+ String(viewportWidth / 1080)
+ );
+ stage.style.height = height + 'px';
+ stageWrap.style.height = scaledHeight + 'px';
+ page.style.minHeight = scaledHeight + 'px';
+ }
+
+ function setTab(tab, updateURL, announce) {
+ state.tab = normalizeTab(tab);
+ page.setAttribute('data-tab', state.tab);
+ stage.setAttribute('data-node-id', DESIGN_FRAME[state.tab]);
+ tabs.forEach(function (button) {
+ var active = button.getAttribute('data-tab-target') === state.tab;
+ button.classList.toggle('is-active', active);
+ button.setAttribute('aria-selected', active ? 'true' : 'false');
+ button.tabIndex = active ? 0 : -1;
+ });
+ panels.forEach(function (panel) {
+ panel.hidden = panel.getAttribute('data-panel') !== state.tab;
+ });
+ updateStageHeight();
+ if (updateURL) replaceTabHash(state.tab);
+ if (announce) {
+ var selected = tabs.find(function (button) {
+ return button.getAttribute('data-tab-target') === state.tab;
+ });
+ document.getElementById('weekStarAnnouncer').textContent = selected
+ ? selected.textContent.trim()
+ : '';
+ }
+ }
+
+ function mockState() {
+ var sample = ASSET_BASE + 'sample-gift.webp';
+ var mockEntries = [1, 2, 3, 4, 5].map(function (rank) {
+ return {
+ rank_no: rank,
+ score: 337700,
+ user: {
+ id: 'mock-user-' + rank,
+ username: rank === 1 ? 'hanneem' : 'star' + rank,
+ avatar: '',
+ },
+ };
+ });
+ var rewardGroups = [1, 2, 3].map(function (rank) {
+ return {
+ rank_no: rank,
+ items: [1, 2, 3].map(function () {
+ return {
+ i18n_key: 'huwaa_weekly_star.frame',
+ duration_days: 7,
+ image_url: sample,
+ };
+ }),
+ };
+ });
+ return {
+ current: {
+ cycle: {
+ cycle_id: 'figma-mock',
+ start_ms: Date.UTC(2025, 11, 27, 21, 0, 0),
+ end_ms: Date.UTC(2026, 0, 3, 20, 59, 0),
+ utc_offset_hours: 3,
+ gifts: [
+ { image_url: sample },
+ { image_url: sample },
+ { image_url: sample },
+ ],
+ rewards: rewardGroups,
+ },
+ },
+ entries: mockEntries,
+ history: [
+ {
+ cycle: {
+ start_ms: Date.UTC(2025, 11, 6, 21, 0, 0),
+ end_ms: Date.UTC(2025, 11, 14, 20, 59, 0),
+ utc_offset_hours: 3,
+ },
+ top_entries: mockEntries,
+ },
+ ],
+ };
+ }
+
+ function loadMock() {
+ var mock = mockState();
+ state.current = mock.current;
+ state.weeklyEntries = mock.entries;
+ state.historyCycles = mock.history;
+ state.loadingCurrent = false;
+ state.loadingWeekly = false;
+ state.loadingHistory = false;
+ state.currentError = '';
+ state.weeklyError = '';
+ state.historyError = '';
+ renderAll();
+ bridgeReady();
+ }
+
+ function tokenAvailable() {
+ return !!(
+ window.HyAppAPI &&
+ window.HyAppAPI.getAccessToken &&
+ window.HyAppAPI.getAccessToken()
+ );
+ }
+
+ function loadReal() {
+ var serial = ++state.requestSerial;
+ state.loadingCurrent = true;
+ state.loadingWeekly = true;
+ state.loadingHistory = true;
+ state.currentError = '';
+ state.weeklyError = '';
+ state.historyError = '';
+ renderAll();
+
+ if (!window.HyAppAPI || !window.HyAppAPI.weeklyStar) {
+ state.loadingCurrent = false;
+ state.loadingWeekly = false;
+ state.loadingHistory = false;
+ state.currentError = 'huwaa_weekly_star.request_failed';
+ state.weeklyError = state.currentError;
+ state.historyError = state.currentError;
+ renderAll();
+ bridgeReady();
+ return;
+ }
+
+ if (!tokenAvailable()) {
+ // Production weekly-star endpoints require the authenticated user context.
+ // Do not send three doomed requests or silently replace the response with mock data.
+ state.loadingCurrent = false;
+ state.loadingWeekly = false;
+ state.loadingHistory = false;
+ state.currentError = 'huwaa_weekly_star.login_required';
+ state.weeklyError = state.currentError;
+ state.historyError = state.currentError;
+ renderAll();
+ bridgeReady();
+ return;
+ }
+
+ // Current/leaderboard and history are intentionally isolated. A history outage
+ // must not erase a successfully loaded current leaderboard, and vice versa.
+ var currentFlow = window.HyAppAPI.weeklyStar
+ .current()
+ .then(function (payload) {
+ if (serial !== state.requestSerial) return;
+ var data = envelope(payload);
+ state.current = data;
+ state.weeklyEntries = data.top_entries || data.topEntries || [];
+ state.loadingCurrent = false;
+ state.loadingWeekly = false;
+ state.currentError = '';
+ state.weeklyError = '';
+ renderHeader();
+ renderWeekly();
+ })
+ .catch(function (error) {
+ if (serial !== state.requestSerial) return;
+ state.current = null;
+ state.weeklyEntries = [];
+ state.loadingCurrent = false;
+ state.loadingWeekly = false;
+ state.currentError = errorKey(error);
+ state.weeklyError = state.currentError;
+ renderHeader();
+ renderWeekly();
+ });
+
+ var historyFlow = window.HyAppAPI.weeklyStar
+ .history(1)
+ .then(function (payload) {
+ if (serial !== state.requestSerial) return;
+ var data = envelope(payload);
+ state.historyCycles = Array.isArray(data)
+ ? data
+ : data.cycles || data.items || data.list || [];
+ state.historyError = '';
+ })
+ .catch(function (error) {
+ if (serial !== state.requestSerial) return;
+ state.historyCycles = [];
+ state.historyError = errorKey(error);
+ })
+ .then(function () {
+ if (serial !== state.requestSerial) return;
+ state.loadingHistory = false;
+ renderHistory();
+ });
+
+ Promise.all([currentFlow, historyFlow]).then(function () {
+ if (serial === state.requestSerial) bridgeReady();
+ });
+ }
+
+ function bridgeReady() {
+ if (!window.HyAppBridge || !window.HyAppBridge.ready) return;
+ window.HyAppBridge.ready({
+ page: 'huwaa_weekly_star',
+ app_code: 'huwaa',
+ mock: useMock,
+ });
+ }
+
+ function activateRelativeTab(currentIndex, direction) {
+ var nextIndex = (currentIndex + direction + tabs.length) % tabs.length;
+ var next = tabs[nextIndex];
+ setTab(next.getAttribute('data-tab-target'), true, true);
+ next.focus();
+ }
+
+ function bindEvents() {
+ tabs.forEach(function (button, index) {
+ button.addEventListener('click', function () {
+ setTab(button.getAttribute('data-tab-target'), true, true);
+ });
+ button.addEventListener('keydown', function (event) {
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
+ event.preventDefault();
+ activateRelativeTab(index, 1);
+ } else if (
+ event.key === 'ArrowLeft' ||
+ event.key === 'ArrowUp'
+ ) {
+ event.preventDefault();
+ activateRelativeTab(index, -1);
+ } else if (event.key === 'Home') {
+ event.preventDefault();
+ setTab('history', true, true);
+ tabs[0].focus();
+ } else if (event.key === 'End') {
+ event.preventDefault();
+ setTab('rewards', true, true);
+ tabs[tabs.length - 1].focus();
+ }
+ });
+ });
+
+ document.addEventListener('click', function (event) {
+ var retry = event.target.closest('[data-retry]');
+ if (retry) {
+ loadReal();
+ return;
+ }
+ var row = event.target.closest('[data-user-id]');
+ if (row && window.HyAppBridge && window.HyAppBridge.openUser) {
+ window.HyAppBridge.openUser(row.getAttribute('data-user-id'));
+ }
+ });
+
+ window.addEventListener('resize', updateStageHeight);
+ window.addEventListener('hashchange', function () {
+ setTab(normalizeTab(tabFromHash()), false, false);
+ });
+ window.addEventListener('hyapp:i18n-ready', function () {
+ // Dynamic API rows are not covered by common/i18n.js's static DOM pass.
+ // Re-rendering keeps empty/error/reward labels synchronized after locale load.
+ renderAll();
+ });
+ }
+
+ function init() {
+ bindEvents();
+ setTab(state.tab, false, false);
+ if (useMock) loadMock();
+ else loadReal();
+ }
+
+ init();
+})();
diff --git a/coin-seller/assets/fami/layers/contact-button-skin.webp b/coin-seller/assets/fami/layers/contact-button-skin.webp
new file mode 100644
index 0000000..57138eb
Binary files /dev/null and b/coin-seller/assets/fami/layers/contact-button-skin.webp differ
diff --git a/coin-seller/assets/fami/layers/flag-ae.webp b/coin-seller/assets/fami/layers/flag-ae.webp
new file mode 100644
index 0000000..39f32cf
Binary files /dev/null and b/coin-seller/assets/fami/layers/flag-ae.webp differ
diff --git a/coin-seller/assets/fami/layers/hero.webp b/coin-seller/assets/fami/layers/hero.webp
new file mode 100644
index 0000000..15593d9
Binary files /dev/null and b/coin-seller/assets/fami/layers/hero.webp differ
diff --git a/coin-seller/assets/fami/layers/manifest.json b/coin-seller/assets/fami/layers/manifest.json
new file mode 100644
index 0000000..ea68b58
--- /dev/null
+++ b/coin-seller/assets/fami/layers/manifest.json
@@ -0,0 +1,112 @@
+{
+ "schemaVersion": 1,
+ "figma": {
+ "fileKey": "OTOg5cwbhVNWxkImGmzTJl",
+ "rootFrame": {
+ "id": "827:3011",
+ "name": "代理充值"
+ },
+ "designSize": {
+ "width": 1080,
+ "height": 2626,
+ "cssWidth": 360,
+ "cssHeight": 875.3333333333
+ },
+ "forbiddenAggregateNodes": [
+ "827:3011",
+ "827:3012",
+ "827:3027",
+ "827:3042",
+ "827:3057",
+ "827:3072",
+ "827:3087",
+ "827:3014",
+ "827:3022"
+ ]
+ },
+ "runtimeRules": {
+ "referenceScreenshot": "coin-seller/.audit/figma-827-3011.png",
+ "referenceScreenshotRuntimeUse": false,
+ "heroMask": {
+ "sourceNode": "827:3089",
+ "sourceStops": [65, 100],
+ "renderedStops": [83.8, 94.7],
+ "purpose": "Reconstruct the transformed Figma mask as CSS; the root-frame screenshot remains QA-only"
+ },
+ "dynamicFields": [
+ "username",
+ "display_user_id",
+ "avatar",
+ "country_code",
+ "country_display_name",
+ "whatsapp_url"
+ ]
+ },
+ "assets": [
+ {
+ "file": "hero.webp",
+ "sourceNode": "827:3090",
+ "sourceName": "image 5017",
+ "sourceType": "RECTANGLE_IMAGE_FILL",
+ "purpose": "Single Figma hero illustration layer; its bottom fade is reconstructed with CSS from node 827:3089",
+ "bounds": { "width": 1080, "height": 1541 },
+ "original": { "width": 1080, "height": 1541 },
+ "exportFormat": "webp-quality-94",
+ "bytes": 479656,
+ "sha256": "b191502302320036ad220a5e3498fe3931079fcd4da71417193f7598a50d4016",
+ "containsDynamicText": false,
+ "containsDecorativeWordmark": true,
+ "containsControls": false,
+ "containsBusinessMedia": false
+ },
+ {
+ "file": "seller-card-skin.webp",
+ "sourceNode": "827:3013",
+ "sourceComponent": "368:1935",
+ "sourceName": "Group 45",
+ "sourceType": "INSTANCE",
+ "purpose": "Decorative gold seller-card border only; all seller content remains DOM/API driven",
+ "bounds": { "width": 1008, "height": 183 },
+ "original": { "width": 1008, "height": 183 },
+ "exportFormat": "webp-lossless",
+ "bytes": 38382,
+ "sha256": "5a442b3d72dc63e03d76941349360395150c87054daa6de3cbeb3ab0bdb48e86",
+ "containsDynamicText": false,
+ "containsDecorativeWordmark": false,
+ "containsControls": false,
+ "containsBusinessMedia": false
+ },
+ {
+ "file": "contact-button-skin.webp",
+ "sourceNode": "827:3023",
+ "sourceName": "按钮",
+ "sourceType": "GROUP",
+ "purpose": "Contact button visual skin without the DOM label or interaction target",
+ "bounds": { "width": 212, "height": 58 },
+ "original": { "width": 212, "height": 58 },
+ "exportFormat": "webp-lossless",
+ "bytes": 8298,
+ "sha256": "7871d8bf5072f9ae99b0c62570261e32fbd5b08005228cd707c75751d30c6c6e",
+ "containsDynamicText": false,
+ "containsDecorativeWordmark": false,
+ "containsControls": false,
+ "containsBusinessMedia": false
+ },
+ {
+ "file": "flag-ae.webp",
+ "sourceNode": "827:3019",
+ "sourceName": "139b9f108330acd1d276942106a294740330946d1121-pVYoL2 1",
+ "sourceType": "RECTANGLE_IMAGE_FILL",
+ "purpose": "UAE flag fallback selected only when the API country_code is AE",
+ "bounds": { "width": 49, "height": 33 },
+ "original": { "width": 1060, "height": 640 },
+ "exportFormat": "webp-lossless",
+ "bytes": 220,
+ "sha256": "e94cadaeef3baab8bfcc8baa520d5d45d42fd11cf8cac52173a4f4552f8db518",
+ "containsDynamicText": false,
+ "containsDecorativeWordmark": false,
+ "containsControls": false,
+ "containsBusinessMedia": false
+ }
+ ]
+}
diff --git a/coin-seller/assets/fami/layers/seller-card-skin.webp b/coin-seller/assets/fami/layers/seller-card-skin.webp
new file mode 100644
index 0000000..81b244d
Binary files /dev/null and b/coin-seller/assets/fami/layers/seller-card-skin.webp differ
diff --git a/coin-seller/fami.css b/coin-seller/fami.css
new file mode 100644
index 0000000..d15f7c4
--- /dev/null
+++ b/coin-seller/fami.css
@@ -0,0 +1,457 @@
+:root {
+ color-scheme: dark;
+ --coin-seller-bg: #06221b;
+ --coin-seller-text: #fff;
+ --coin-seller-muted: rgba(255, 255, 255, 0.7);
+ --coin-seller-gold: #ffebbb;
+ --coin-seller-button-text: #773924;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ width: 100%;
+ min-height: 100%;
+ margin: 0;
+}
+
+body {
+ min-height: 100svh;
+ overflow-x: hidden;
+ background: #020b09;
+ color: var(--coin-seller-text);
+ font-family:
+ 'Source Han Sans SC',
+ 'Noto Sans SC',
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Arial,
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: geometricPrecision;
+}
+
+button,
+a {
+ font: inherit;
+ -webkit-tap-highlight-color: transparent;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.coin-seller-page {
+ width: min(100%, 480px);
+ min-height: 100svh;
+ margin: 0 auto;
+ overflow: clip;
+ background: var(--coin-seller-bg);
+ container-type: inline-size;
+}
+
+.coin-seller-stage {
+ position: relative;
+ min-height: max(100svh, 243.1481cqw);
+ isolation: isolate;
+}
+
+.coin-seller-hero {
+ position: relative;
+ z-index: 0;
+ width: 100%;
+ height: 142.5cqw;
+ overflow: hidden;
+ pointer-events: none;
+ -webkit-mask-image: linear-gradient(
+ to bottom,
+ #000 0%,
+ #000 83.8%,
+ transparent 94.7%,
+ transparent 100%
+ );
+ mask-image: linear-gradient(
+ to bottom,
+ #000 0%,
+ #000 83.8%,
+ transparent 94.7%,
+ transparent 100%
+ );
+}
+
+.coin-seller-hero img {
+ position: absolute;
+ inset: 0 auto auto 0;
+ display: block;
+ width: 100%;
+ height: auto;
+ user-select: none;
+}
+
+.coin-seller-region {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ margin-top: -7.5cqw;
+ padding: 0 3.3333cqw 11.5741cqw;
+}
+
+.seller-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2.963cqw;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+.seller-card,
+.seller-skeleton-card {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1008 / 183;
+ overflow: visible;
+}
+
+.seller-card-skin,
+.seller-skeleton-skin {
+ position: absolute;
+ z-index: 0;
+ inset: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ user-select: none;
+}
+
+.seller-avatar-shell {
+ position: absolute;
+ z-index: 1;
+ left: 3.6706%;
+ top: 17.4863%;
+ width: 11.8056%;
+ aspect-ratio: 1;
+ overflow: hidden;
+ border: 0.2778cqw solid var(--coin-seller-gold);
+ border-radius: 50%;
+ background: #0e4136;
+}
+
+.seller-avatar,
+.seller-avatar-fallback {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.seller-avatar {
+ z-index: 1;
+ inset: auto;
+ left: -17.83%;
+ top: -8.53%;
+ display: block;
+ width: 123.26%;
+ height: 164.36%;
+ object-fit: cover;
+ opacity: 0;
+ transition: opacity 120ms ease;
+}
+
+.seller-avatar.is-loaded {
+ opacity: 1;
+}
+
+.seller-avatar-fallback {
+ display: grid;
+ place-items: center;
+ color: rgba(255, 235, 187, 0.92);
+ background: #0f5548;
+ font-size: 4.1667cqw;
+ font-weight: 700;
+ line-height: 1;
+ text-transform: uppercase;
+}
+
+.seller-country {
+ position: absolute;
+ z-index: 1;
+ left: 17.8571%;
+ top: 25.6831%;
+ width: 4.8611%;
+ height: 18.0328%;
+ overflow: hidden;
+ border-radius: 0.2778cqw;
+}
+
+.seller-flag,
+.seller-flag-code {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.seller-flag {
+ z-index: 1;
+ display: block;
+ object-fit: cover;
+ opacity: 0;
+}
+
+.seller-flag.is-loaded {
+ opacity: 1;
+}
+
+.seller-flag-code {
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ color: #fff;
+ background: #17483d;
+ font-size: 1.3889cqw;
+ font-weight: 800;
+ line-height: 1;
+}
+
+.seller-name,
+.seller-display-id {
+ position: absolute;
+ z-index: 1;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ direction: auto;
+}
+
+.seller-name {
+ left: 24.3056%;
+ top: 24.0437%;
+ right: 27.4802%;
+ color: var(--coin-seller-text);
+ font-size: 3.7037cqw;
+ font-weight: 500;
+ line-height: 3.3333cqw;
+}
+
+.seller-display-id {
+ left: 17.8571%;
+ top: 56.2842%;
+ right: 34%;
+ color: var(--coin-seller-muted);
+ font-size: 2.7778cqw;
+ font-weight: 400;
+ line-height: 3.3333cqw;
+}
+
+.seller-contact,
+.seller-retry {
+ position: absolute;
+ z-index: 2;
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ padding: 0;
+ border: 0;
+ color: var(--coin-seller-button-text);
+ background: transparent;
+ text-align: center;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.seller-contact {
+ left: 74.0079%;
+ top: 33.8798%;
+ width: 21.0317%;
+ height: 31.694%;
+ border-radius: 999px;
+ font-size: 2.7778cqw;
+ font-weight: 700;
+ line-height: 3.3333cqw;
+}
+
+.seller-contact img,
+.seller-retry img {
+ position: absolute;
+ z-index: -1;
+ inset: 0;
+ display: block;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ user-select: none;
+}
+
+.seller-contact span,
+.seller-retry span {
+ position: relative;
+ z-index: 1;
+ max-width: 90%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.seller-contact[aria-disabled='true'] {
+ cursor: default;
+ filter: saturate(0.42);
+ opacity: 0.58;
+}
+
+.seller-contact:focus-visible,
+.seller-retry:focus-visible {
+ outline: 0.5556cqw solid #fff;
+ outline-offset: 0.5556cqw;
+}
+
+.seller-message {
+ display: grid;
+ min-height: 33.8889cqw;
+ place-items: center;
+ padding: 6.6667cqw;
+ border: 0.2778cqw solid rgba(255, 235, 187, 0.78);
+ background: rgba(3, 38, 30, 0.94);
+ box-shadow: inset 0 0 0 0.8333cqw rgba(141, 64, 34, 0.44);
+ text-align: center;
+}
+
+.seller-message p {
+ max-width: 78%;
+ margin: 0;
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 3.3333cqw;
+ font-weight: 500;
+ line-height: 1.45;
+}
+
+.seller-error {
+ padding-bottom: 14.4444cqw;
+}
+
+.seller-retry {
+ left: 50%;
+ bottom: 5.5556cqw;
+ width: 21.0317cqw;
+ height: 5.3704cqw;
+ transform: translateX(-50%);
+ border-radius: 999px;
+ font-size: 2.7778cqw;
+ font-weight: 700;
+ line-height: 3.3333cqw;
+}
+
+.seller-skeleton-card {
+ overflow: hidden;
+}
+
+.seller-skeleton-avatar,
+.seller-skeleton-line,
+.seller-skeleton-button {
+ position: absolute;
+ z-index: 1;
+ overflow: hidden;
+ background: rgba(255, 255, 255, 0.12);
+}
+
+.seller-skeleton-avatar::after,
+.seller-skeleton-line::after,
+.seller-skeleton-button::after {
+ position: absolute;
+ inset: 0;
+ content: '';
+ background: linear-gradient(
+ 100deg,
+ transparent 15%,
+ rgba(255, 235, 187, 0.32) 50%,
+ transparent 85%
+ );
+ transform: translateX(-110%);
+ animation: seller-shimmer 1.4s ease-in-out infinite;
+}
+
+.seller-skeleton-avatar {
+ left: 3.6706%;
+ top: 17.4863%;
+ width: 11.8056%;
+ aspect-ratio: 1;
+ border: 0.2778cqw solid rgba(255, 235, 187, 0.62);
+ border-radius: 50%;
+}
+
+.seller-skeleton-line {
+ left: 17.8571%;
+ height: 12%;
+ border-radius: 999px;
+}
+
+.seller-skeleton-name {
+ top: 27%;
+ width: 43%;
+}
+
+.seller-skeleton-id {
+ top: 58%;
+ width: 26%;
+}
+
+.seller-skeleton-button {
+ left: 74.0079%;
+ top: 33.8798%;
+ width: 21.0317%;
+ height: 31.694%;
+ border-radius: 999px;
+}
+
+@keyframes seller-shimmer {
+ to {
+ transform: translateX(110%);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .seller-avatar {
+ transition: none;
+ }
+
+ .seller-skeleton-avatar::after,
+ .seller-skeleton-line::after,
+ .seller-skeleton-button::after {
+ animation: none;
+ }
+}
+
+@supports not (container-type: inline-size) {
+ .coin-seller-stage {
+ min-height: max(100svh, 243.1481vw);
+ }
+
+ .coin-seller-hero {
+ height: 142.5vw;
+ }
+
+ .coin-seller-region {
+ margin-top: -7.5vw;
+ padding-inline: 3.3333vw;
+ padding-bottom: 11.5741vw;
+ }
+}
diff --git a/coin-seller/fami.html b/coin-seller/fami.html
new file mode 100644
index 0000000..dec5260
--- /dev/null
+++ b/coin-seller/fami.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+ Coins Seller
+
+
+
+
+
+
+
+
+

+
+
+
+
+
Loading coin sellers
+
+
+
+
+
+ No coin sellers are available in your region.
+
+
+
+
+
+ Unable to load coin sellers.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
![]()
+
+
+
+
+
+ Contact
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/coin-seller/fami.js b/coin-seller/fami.js
new file mode 100644
index 0000000..38fa782
--- /dev/null
+++ b/coin-seller/fami.js
@@ -0,0 +1,368 @@
+(function () {
+ 'use strict';
+
+ var page = document.getElementById('coinSellerPage');
+ var region = document.getElementById('coinSellerRegion');
+ var loadingView = document.getElementById('sellerLoading');
+ var emptyView = document.getElementById('sellerEmpty');
+ var errorView = document.getElementById('sellerError');
+ var retryButton = document.getElementById('sellerRetry');
+ var skeletonList = document.getElementById('sellerSkeletonList');
+ var sellerList = document.getElementById('sellerList');
+ var sellerTemplate = document.getElementById('sellerCardTemplate');
+ var params =
+ (window.HyAppParams &&
+ window.HyAppParams.current &&
+ window.HyAppParams.current.raw) ||
+ new URLSearchParams(window.location.search);
+ var requestSequence = 0;
+ var currentItems = [];
+ var currentView = 'loading';
+
+ if (!page || !region || !sellerList || !sellerTemplate) return;
+
+ function t(key, fallback) {
+ return window.HyAppI18n && window.HyAppI18n.t
+ ? window.HyAppI18n.t(key, fallback)
+ : fallback;
+ }
+
+ function format(message, values) {
+ return String(message || '').replace(
+ /\{([a-zA-Z0-9_]+)\}/g,
+ function (_, key) {
+ return Object.prototype.hasOwnProperty.call(values || {}, key)
+ ? String(values[key])
+ : '';
+ }
+ );
+ }
+
+ function stringValue(value) {
+ return value === undefined || value === null
+ ? ''
+ : String(value).trim();
+ }
+
+ function safeMediaURL(value) {
+ var raw = stringValue(value);
+ if (!raw) return '';
+ try {
+ var parsed = new URL(raw, window.location.href);
+ return parsed.protocol === 'https:' || parsed.protocol === 'http:'
+ ? parsed.href
+ : '';
+ } catch (_) {
+ return '';
+ }
+ }
+
+ function safeWhatsAppURL(value) {
+ var raw = stringValue(value);
+ if (!raw) return '';
+ try {
+ var parsed = new URL(raw, window.location.href);
+ if (parsed.protocol === 'whatsapp:') return parsed.href;
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
+ return '';
+ }
+ var host = parsed.hostname.toLowerCase();
+ return host === 'wa.me' || host === 'api.whatsapp.com'
+ ? parsed.href
+ : '';
+ } catch (_) {
+ return '';
+ }
+ }
+
+ function isVisualFixture() {
+ return params.get('mock') === '1';
+ }
+
+ function visualFixturePayload() {
+ var requestedCount = Number(params.get('mock_count') || 5);
+ var count = Number.isFinite(requestedCount)
+ ? Math.max(0, Math.min(20, Math.floor(requestedCount)))
+ : 5;
+ var avatar = safeMediaURL(params.get('mock_avatar'));
+ var contactEnabled = params.get('mock_contact') !== '0';
+ var items = [];
+
+ // 该分支只服务显式 mock=1 的设计验收。生产请求失败时绝不进入这里,
+ // 否则示例币商会掩盖鉴权、区域或上游故障并被用户误认为真实联系人。
+ for (var index = 0; index < count; index += 1) {
+ items.push({
+ user_id: String(8800000 + index),
+ display_user_id: String(123456789 + index),
+ username: params.get('mock_name') || 'Namenamenamenamename',
+ avatar: avatar,
+ country_code: 'AE',
+ country_name: 'United Arab Emirates',
+ country_display_name: 'United Arab Emirates',
+ whatsapp_phone: contactEnabled ? '+971500000000' : '',
+ whatsapp_url: contactEnabled
+ ? 'https://wa.me/971500000000'
+ : '',
+ });
+ }
+ return { items: items, total: items.length };
+ }
+
+ function normalizePayload(payload) {
+ if (!payload || !Array.isArray(payload.items)) {
+ throw new Error('coin_seller_response_invalid');
+ }
+ return payload.items.map(function (item) {
+ var source = item && typeof item === 'object' ? item : {};
+ return {
+ userId: stringValue(source.user_id),
+ displayUserId: stringValue(source.display_user_id),
+ username: stringValue(source.username),
+ avatar: stringValue(source.avatar),
+ countryCode: stringValue(source.country_code).toUpperCase(),
+ countryName: stringValue(
+ source.country_display_name || source.country_name
+ ),
+ whatsappPhone: stringValue(source.whatsapp_phone),
+ whatsappURL: stringValue(source.whatsapp_url),
+ };
+ });
+ }
+
+ function sellerName(item) {
+ return (
+ item.username ||
+ item.displayUserId ||
+ item.userId ||
+ t('coin_seller_fami.unknown_seller', 'Seller')
+ );
+ }
+
+ function firstVisibleCharacter(value) {
+ var text = stringValue(value);
+ if (!text) return '';
+ if (window.Intl && window.Intl.Segmenter) {
+ var segments = new window.Intl.Segmenter(undefined, {
+ granularity: 'grapheme',
+ }).segment(text);
+ var first = segments[Symbol.iterator]().next();
+ return first.done ? '' : first.value.segment;
+ }
+ return Array.from(text)[0] || '';
+ }
+
+ function flagURL(countryCode) {
+ var code = stringValue(countryCode).toUpperCase();
+ if (!/^[A-Z]{2}$/.test(code)) return '';
+ if (code === 'AE') return './assets/fami/layers/flag-ae.webp';
+ // Figma 只提供 AE 叶子素材;其他国家必须按接口 country_code 请求真实国旗,
+ // 不能错误复用 AE 图。失败时保留 ISO code 作为有意义的单卡降级。
+ return 'https://flagcdn.com/w80/' + code.toLowerCase() + '.png';
+ }
+
+ function bindAvatar(card, item, name) {
+ var image = card.querySelector('.seller-avatar');
+ var fallback = card.querySelector('.seller-avatar-fallback');
+ var source = safeMediaURL(item.avatar);
+ fallback.textContent = firstVisibleCharacter(name);
+ image.alt = format(
+ t('coin_seller_fami.avatar_label', '{name} avatar'),
+ { name: name }
+ );
+ image.addEventListener(
+ 'load',
+ function () {
+ image.classList.add('is-loaded');
+ },
+ { once: true }
+ );
+ image.addEventListener(
+ 'error',
+ function () {
+ image.classList.remove('is-loaded');
+ image.removeAttribute('src');
+ },
+ { once: true }
+ );
+ if (source) image.src = source;
+ }
+
+ function bindFlag(card, item) {
+ var image = card.querySelector('.seller-flag');
+ var fallback = card.querySelector('.seller-flag-code');
+ var code = item.countryCode;
+ var source = flagURL(code);
+ var country = item.countryName || code;
+ fallback.textContent = code;
+ image.alt = country
+ ? format(t('coin_seller_fami.country_flag', '{country} flag'), {
+ country: country,
+ })
+ : '';
+ image.addEventListener(
+ 'load',
+ function () {
+ image.classList.add('is-loaded');
+ },
+ { once: true }
+ );
+ image.addEventListener(
+ 'error',
+ function () {
+ image.classList.remove('is-loaded');
+ image.removeAttribute('src');
+ },
+ { once: true }
+ );
+ if (source) image.src = source;
+ }
+
+ function bindContact(card, item, name) {
+ var contact = card.querySelector('.seller-contact');
+ var label = contact.querySelector('span');
+ var target = safeWhatsAppURL(item.whatsappURL);
+ var contactText = t('coin_seller_fami.contact', 'Contact');
+ label.textContent = contactText;
+
+ if (target) {
+ contact.href = target;
+ contact.target = '_blank';
+ contact.removeAttribute('aria-disabled');
+ contact.removeAttribute('tabindex');
+ contact.setAttribute(
+ 'aria-label',
+ format(t('coin_seller_fami.contact_seller', 'Contact {name}'), {
+ name: name,
+ })
+ );
+ return;
+ }
+
+ // 空联系方式来自服务端的稳定 schema。控件必须真实禁用,而不是仍然
+ // 导航到空页或由客户端根据电话号码猜测一个 WhatsApp 地址。
+ contact.removeAttribute('href');
+ contact.removeAttribute('target');
+ contact.setAttribute('aria-disabled', 'true');
+ contact.setAttribute('tabindex', '-1');
+ contact.setAttribute(
+ 'aria-label',
+ t('coin_seller_fami.contact_unavailable', 'Contact unavailable')
+ );
+ contact.addEventListener('click', function (event) {
+ event.preventDefault();
+ });
+ }
+
+ function createSellerCard(item, index) {
+ var fragment = sellerTemplate.content.cloneNode(true);
+ var card = fragment.querySelector('.seller-card');
+ var name = sellerName(item);
+ var visibleID = item.displayUserId || item.userId;
+
+ card.dataset.userId = item.userId;
+ card.dataset.index = String(index);
+ card.querySelector('.seller-name').textContent = name;
+ card.querySelector('.seller-display-id').textContent = format(
+ t('coin_seller_fami.seller_id', 'ID: {id}'),
+ { id: visibleID }
+ );
+
+ bindAvatar(card, item, name);
+ bindFlag(card, item);
+ bindContact(card, item, name);
+ return fragment;
+ }
+
+ function renderSellers(items) {
+ var fragment = document.createDocumentFragment();
+ sellerList.replaceChildren();
+ items.forEach(function (item, index) {
+ fragment.appendChild(createSellerCard(item, index));
+ });
+ sellerList.appendChild(fragment);
+ }
+
+ function createSkeletonCard() {
+ var item = document.createElement('li');
+ item.className = 'seller-skeleton-card';
+ item.innerHTML =
+ '
' +
+ '' +
+ '' +
+ '' +
+ '';
+ return item;
+ }
+
+ function renderSkeletons() {
+ var fragment = document.createDocumentFragment();
+ for (var index = 0; index < 5; index += 1) {
+ fragment.appendChild(createSkeletonCard());
+ }
+ skeletonList.replaceChildren(fragment);
+ }
+
+ function setView(view) {
+ currentView = view;
+ loadingView.hidden = view !== 'loading';
+ emptyView.hidden = view !== 'empty';
+ errorView.hidden = view !== 'error';
+ sellerList.hidden = view !== 'list';
+ region.setAttribute('aria-busy', String(view === 'loading'));
+ }
+
+ function requestCoinSellers() {
+ if (isVisualFixture()) {
+ return Promise.resolve(visualFixturePayload());
+ }
+ if (
+ !window.HyAppAPI ||
+ !window.HyAppAPI.wallet ||
+ !window.HyAppAPI.wallet.listCoinSellers
+ ) {
+ return Promise.reject(new Error('coin_seller_api_unavailable'));
+ }
+ return window.HyAppAPI.wallet.listCoinSellers();
+ }
+
+ function loadCoinSellers() {
+ var sequence = ++requestSequence;
+ currentItems = [];
+ setView('loading');
+
+ requestCoinSellers()
+ .then(function (payload) {
+ // Retry 或后续刷新可能先于旧请求完成;只允许最新序列提交 UI,
+ // 防止慢失败覆盖已经成功的新列表。
+ if (sequence !== requestSequence) return;
+ currentItems = normalizePayload(payload);
+ if (!currentItems.length) {
+ setView('empty');
+ return;
+ }
+ renderSellers(currentItems);
+ setView('list');
+ })
+ .catch(function (error) {
+ if (sequence !== requestSequence) return;
+ currentItems = [];
+ setView('error');
+ if (window.console && window.console.warn) {
+ window.console.warn('[coin-seller:fami]', error);
+ }
+ });
+ }
+
+ retryButton.addEventListener('click', loadCoinSellers);
+
+ window.addEventListener('hyapp:i18n-ready', function () {
+ // DOM 模板里的动态 ID 与可访问标签不由 data-i18n 自动插值;语言切换后
+ // 重新渲染当前成功列表即可保持视觉与读屏文案一致,不重新发业务请求。
+ if (currentView === 'list' && currentItems.length) {
+ renderSellers(currentItems);
+ }
+ });
+
+ renderSkeletons();
+ loadCoinSellers();
+})();
diff --git a/common/api.js b/common/api.js
index 7e92451..f9314f9 100644
--- a/common/api.js
+++ b/common/api.js
@@ -518,6 +518,17 @@ var default_api = 'https://api.global-interaction.com/';
body: payload || {},
});
},
+ settings: function () {
+ return request('/api/v1/vip/settings', { method: 'GET' });
+ },
+ updateSettings: function (payload) {
+ // PATCH 只发送用户实际修改的开关;省略字段必须由 wallet-service 保留原值,
+ // 不能在 H5 端用默认值覆盖另一个端刚写入的设置。
+ return request('/api/v1/vip/settings', {
+ method: 'PATCH',
+ body: payload || {},
+ });
+ },
};
var userAPI = {
@@ -617,6 +628,13 @@ var default_api = 'https://api.global-interaction.com/';
query: params || {},
});
},
+ listCoinSellers: function () {
+ // 区域边界只能由 gateway 根据 JWT 当前用户解析;H5 不传 region_id,
+ // 避免页面参数被篡改后读取到其他区域的币商联系方式。
+ return request('/api/v1/wallet/coin-sellers', {
+ method: 'GET',
+ });
+ },
transferCoinFromSeller: function (payload) {
return request('/api/v1/wallet/coin-seller/transfer', {
method: 'POST',
diff --git a/common/locales/ar.json b/common/locales/ar.json
index db08d05..879962f 100644
--- a/common/locales/ar.json
+++ b/common/locales/ar.json
@@ -55,6 +55,9 @@
"roomReward.idPrefix": "المعرّف:",
"roomReward.roomOwner": "مالك الغرفة",
"roomReward.changeLanguage": "تغيير اللغة",
+ "roomReward.loading": "جارٍ التحميل...",
+ "roomReward.empty": "لا توجد مستويات مكافآت بعد",
+ "roomReward.loadFailed": "فشل التحميل",
"rechargeReward.pageTitle": "مكافأة الشحن التراكمي",
"rechargeReward.pageLabel": "فعالية مكافأة الشحن التراكمي",
"rechargeReward.changeLanguage": "تغيير اللغة",
@@ -66,6 +69,7 @@
"rechargeReward.accumulatedRecharge": "إجمالي الشحن:",
"rechargeReward.rechargeTitle": "اشحن {amount}",
"rechargeReward.claim": "استلام",
+ "rechargeReward.receive": "استلام",
"rechargeReward.claiming": "جار الاستلام...",
"rechargeReward.claimed": "تم الاستلام",
"rechargeReward.claimSuccess": "تم الاستلام",
@@ -534,6 +538,17 @@
"cp.rewardCoins10000": "عملات*10000",
"cp.rewardCount7": "عدّاد*7 أيام",
"cp.copyright": "حقوق هذا النشاط تعود إلى Shila",
+ "cp.countdownLabel": "العد التنازلي للنشاط",
+ "cp.huwaaPlazaGiftTitle": "ستظهر هنا الهدايا المتبادلة بين ثنائيات CP بقيمة 5000 ذهب أو أكثر",
+ "cp.previousNo1PositionTitle": "سجلات المركز الأول السابقة",
+ "cp.previousNo1Subtitle": "(عرض آخر 10 سجلات مرتبة تصاعديًا.)",
+ "cp.rankPodiumLabel": "ترتيب أفضل ثنائيات CP",
+ "cp.loading": "جارٍ التحميل...",
+ "cp.empty": "لا توجد بيانات بعد.",
+ "cp.error": "تعذر تحميل بيانات النشاط.",
+ "cp.retry": "إعادة المحاولة",
+ "cp.reward": "مكافأة",
+ "cp.daysLong": "أيام",
"recharge.pageTitle": "Recharge Center",
"recharge.pageSubtitle": "اختر باقة عملات وأكمل الدفع.",
"recharge.changeLanguage": "تغيير اللغة",
@@ -800,5 +815,155 @@
"fami_rank.network_error": "تعذر تحميل التصنيف. حاول مرة أخرى.",
"fami_rank.unknown": "غير معروف",
"huwaa_rank.page_title": "تصنيف هوا",
- "huwaa_rank.game": "اللعبة"
+ "huwaa_rank.game": "اللعبة",
+ "fami_vip.back": "رجوع",
+ "fami_vip.settings": "إعدادات VIP",
+ "fami_vip.help": "مساعدة VIP",
+ "fami_vip.levels": "مستويات VIP",
+ "fami_vip.exclusive_outfit": "المظهر الحصري",
+ "fami_vip.vip_privileges": "امتيازات VIP",
+ "fami_vip.no_outfits": "لا توجد مظاهر حصرية لهذا المستوى.",
+ "fami_vip.no_privileges": "لا توجد امتيازات لهذا المستوى.",
+ "fami_vip.outfit": "مظهر VIP",
+ "fami_vip.loading": "جارٍ التحميل...",
+ "fami_vip.retry": "إعادة المحاولة",
+ "fami_vip.purchase_region": "شراء VIP",
+ "fami_vip.close": "إغلاق",
+ "fami_vip.help_title": "مساعدة VIP",
+ "fami_vip.help_body": "يتم توفير الامتيازات وإمكانية الشراء للمستوى المحدد من خدمة VIP.",
+ "fami_vip.settings_title": "إعدادات VIP",
+ "fami_vip.room_entry_notice": "إشعار دخول الغرفة",
+ "fami_vip.online_notice": "إشعار الظهور العام",
+ "fami_vip.unlocked": "مفتوح",
+ "fami_vip.locked": "مقفل",
+ "fami_vip.days": "{count} يومًا",
+ "fami_vip.no_expiry": "بلا انتهاء",
+ "fami_vip.trial_level": "المستوى التجريبي: {level}",
+ "fami_vip.unlocked_level": "المستوى المفتوح: {level}",
+ "fami_vip.valid_until": "صالح حتى: {date}",
+ "fami_vip.renew_vip": "تجديد VIP",
+ "fami_vip.upgrade_vip": "ترقية VIP",
+ "fami_vip.activate_vip": "تفعيل VIP",
+ "fami_vip.unavailable": "غير متاح",
+ "fami_vip.current_vip_higher": "لا يمكن شراء مستوى VIP أقل أثناء تفعيل مستوى أعلى.",
+ "fami_vip.processing": "جارٍ التنفيذ...",
+ "fami_vip.purchase_success": "تم الشراء بنجاح.",
+ "fami_vip.purchase_failed": "فشل الشراء.",
+ "fami_vip.version_changed": "تم تحديث إعدادات VIP. أعد المحاولة.",
+ "fami_vip.load_failed": "تعذر تحميل بيانات VIP.",
+ "fami_vip.settings_saved": "تم حفظ الإعدادات.",
+ "fami_vip.settings_failed": "تعذر حفظ الإعدادات.",
+ "huwaa_invite.page_title": "عجلة النقود",
+ "huwaa_invite.page_aria": "نشاط دعوة عجلة النقود في هوا",
+ "huwaa_invite.back": "رجوع",
+ "huwaa_invite.wallet_aria": "نقاط عجلة النقود",
+ "huwaa_invite.points_left": "النقاط المتبقية للسحب:",
+ "huwaa_invite.withdraw": "سحب إلى المحفظة",
+ "huwaa_invite.wheel_aria": "عجلة النقود",
+ "huwaa_invite.estimated_rewards": "المكافآت المتوقعة",
+ "huwaa_invite.medium": "متوسط",
+ "huwaa_invite.spin": "تدوير",
+ "huwaa_invite.invite_now": "ادعُ الأصدقاء الآن",
+ "huwaa_invite.tasks_aria": "مهام الدعوة",
+ "huwaa_invite.close": "إغلاق",
+ "huwaa_invite.congratulations": "تهانينا!",
+ "huwaa_invite.you_won": "لقد ربحت",
+ "huwaa_invite.awesome": "رائع",
+ "huwaa_invite.completed": "مكتمل",
+ "huwaa_invite.go": "اذهب",
+ "huwaa_invite.reward": "مكافأة",
+ "huwaa_invite.login_required": "يرجى تسجيل الدخول أولاً",
+ "huwaa_invite.request_failed": "فشل الطلب",
+ "huwaa_invite.open_app_task": "افتح هذه المهمة في هوا",
+ "huwaa_invite.invite_unavailable": "رابط الدعوة غير متاح بعد",
+ "huwaa_invite.copied": "تم النسخ",
+ "huwaa_invite.copy_failed": "فشل النسخ",
+ "huwaa_invite.open_wallet": "افتح محفظة هوا للسحب",
+ "huwaa_invite.task_title": "استخدم الميكروفون لمدة دقيقة",
+ "huwaa_invite.task_description": "يكمل الصديق المصادقة الحيوية",
+ "huwaa_invite.medium_reward": "مكافأة متوسطة",
+ "huwaa_invite.points_label": "نقطة",
+ "huwaa_weekly_star.page_title": "نجم الأسبوع",
+ "huwaa_weekly_star.page_aria": "فعالية نجم الأسبوع في هوا",
+ "huwaa_weekly_star.activity_note": "ملاحظة: أرسل الهدايا الثلاث التالية للمشاركة في التصنيف. سيحصل المستخدمون ضمن المراكز الثلاثة الأولى على مكافآت.",
+ "huwaa_weekly_star.gifts_aria": "هدايا الفعالية",
+ "huwaa_weekly_star.gift_label": "هدية الفعالية {index}",
+ "huwaa_weekly_star.tabs_aria": "أقسام نجم الأسبوع",
+ "huwaa_weekly_star.previous_top_three": "سجلات الثلاثة الأوائل السابقة",
+ "huwaa_weekly_star.this_week_rank": "تصنيف هذا الأسبوع",
+ "huwaa_weekly_star.reward_description": "تفاصيل المكافآت",
+ "huwaa_weekly_star.loading": "جارٍ التحميل…",
+ "huwaa_weekly_star.no_active_cycle": "لا توجد دورة نشطة",
+ "huwaa_weekly_star.no_history": "لا توجد سجلات تصنيف سابقة.",
+ "huwaa_weekly_star.no_ranking": "لا توجد بيانات تصنيف حتى الآن.",
+ "huwaa_weekly_star.no_rewards": "لا توجد بيانات مكافآت حتى الآن.",
+ "huwaa_weekly_star.no_rank_reward": "لا توجد بيانات مكافأة.",
+ "huwaa_weekly_star.login_required": "سجّل الدخول لعرض نجم الأسبوع.",
+ "huwaa_weekly_star.profile_required": "أكمل ملفك الشخصي لعرض نجم الأسبوع.",
+ "huwaa_weekly_star.request_failed": "تعذر تحميل نجم الأسبوع. أعد المحاولة.",
+ "huwaa_weekly_star.retry": "إعادة المحاولة",
+ "huwaa_weekly_star.unknown_user": "مستخدم",
+ "huwaa_weekly_star.rank_label": "الترتيب {rank}، {name}، {score}",
+ "huwaa_weekly_star.reward": "مكافأة",
+ "huwaa_weekly_star.frame": "إطار",
+ "huwaa_weekly_star.reward_days": "{name} ({days} أيام)",
+ "coin_seller_fami.page_title": "بائعو العملات",
+ "coin_seller_fami.page_aria": "قائمة بائعي العملات",
+ "coin_seller_fami.list_aria": "بائعو العملات المتاحون",
+ "coin_seller_fami.loading": "جارٍ تحميل بائعي العملات",
+ "coin_seller_fami.empty": "لا يوجد بائعو عملات متاحون في منطقتك.",
+ "coin_seller_fami.load_failed": "تعذر تحميل بائعي العملات.",
+ "coin_seller_fami.retry": "إعادة المحاولة",
+ "coin_seller_fami.contact": "تواصل",
+ "coin_seller_fami.seller_id": "المعرّف: {id}",
+ "coin_seller_fami.unknown_seller": "بائع",
+ "coin_seller_fami.avatar_label": "الصورة الشخصية لـ {name}",
+ "coin_seller_fami.country_flag": "علم {country}",
+ "coin_seller_fami.contact_seller": "تواصل مع {name}",
+ "coin_seller_fami.contact_unavailable": "التواصل غير متاح",
+ "fami_week_star.page_title": "نجمة الأسبوع",
+ "fami_week_star.page_aria": "فعالية نجمة الأسبوع من Fami",
+ "fami_week_star.hero_title": "نجمة الأسبوع",
+ "fami_week_star.note": "ملاحظة: أرسل الهدايا الثلاث التالية للمشاركة في التصنيف. سيحصل المستخدمون الذين يحتلون المراكز الثلاثة الأولى في لوحة الصدارة على مكافآت.",
+ "fami_week_star.gifts_aria": "هدايا الفعالية",
+ "fami_week_star.tabs_aria": "أقسام نجمة الأسبوع",
+ "fami_week_star.tab_history": "سجلات أفضل 3 السابقة",
+ "fami_week_star.tab_weekly": "تصنيف هذا الأسبوع",
+ "fami_week_star.tab_rewards": "وصف المكافآت",
+ "fami_week_star.loading": "جارٍ التحميل...",
+ "fami_week_star.load_failed": "تعذر تحميل التصنيف.",
+ "fami_week_star.unknown": "غير معروف",
+ "fami_week_star.rank_aria": "المرتبة {rank}",
+ "fami_week_star.entry_aria": "المرتبة {rank}، {name}، {score}",
+ "fami_week_star.empty_weekly": "لا توجد بيانات تصنيف بعد.",
+ "fami_week_star.empty_history": "لا توجد سجلات تصنيف سابقة.",
+ "fami_week_star.gift": "هدية",
+ "fami_week_star.coins": "عملات",
+ "fami_week_star.reward": "مكافأة",
+ "fami_week_star.reward_days": "{name} ({days} أيام)",
+ "fami_week_star.top_rewards_aria": "مكافآت المركز {rank}",
+ "fami_week_star.activity_load_failed": "تعذر تحميل نجمة الأسبوع.",
+ "fami_week_star.no_active_cycle": "لا توجد دورة نشطة.",
+ "fami_week_star.empty_rewards": "لا توجد بيانات مكافآت بعد.",
+ "fami_week_star.mock_reward_name": "إطار",
+ "cp.disabled": "هذا النشاط غير متاح.",
+ "cp.unavailable": "مصدر البيانات غير متاح بعد.",
+ "cp.previousWeekTopTitle": "أفضل 3 سابقًا",
+ "cp.previousWeekTopSubtitle": "(أفضل ثنائيات CP من الأسبوع المكتمل السابق.)",
+ "cp.rankPlace": "الترتيب",
+ "cp.rewardTierGold": "جوائز المركز الأول",
+ "cp.rewardTierSilver": "جوائز المركز الثاني",
+ "cp.rewardTierBronze": "جوائز المركز الثالث",
+ "cp.rewardAvatarFrame": "إطار الصورة الرمزية",
+ "cp.rewardProfileCard": "بطاقة الملف الشخصي",
+ "cp.rewardCoins": "عملات",
+ "cp.rewardVehicle": "مركبة",
+ "cp.rewardChatBubble": "فقاعة دردشة",
+ "cp.rewardBadge": "شارة",
+ "cp.rewardFloatingScreen": "شاشة عائمة",
+ "cp.rewardGift": "هدية",
+ "cp.rewardMicSeatIcon": "أيقونة مقعد الميكروفون",
+ "cp.rewardMicSeatAnimation": "حركة مقعد الميكروفون",
+ "cp.rewardEmojiPack": "حزمة رموز تعبيرية",
+ "cp.rewardVipTrialCard": "بطاقة VIP تجريبية"
}
diff --git a/common/locales/en.json b/common/locales/en.json
index 5666231..b0dcd4c 100644
--- a/common/locales/en.json
+++ b/common/locales/en.json
@@ -55,6 +55,9 @@
"roomReward.idPrefix": "ID:",
"roomReward.roomOwner": "Room owner",
"roomReward.changeLanguage": "Change language",
+ "roomReward.loading": "Loading...",
+ "roomReward.empty": "No reward tiers yet",
+ "roomReward.loadFailed": "Load failed",
"rechargeReward.pageTitle": "Recharge Reward",
"rechargeReward.pageLabel": "Recharge Reward activity",
"rechargeReward.changeLanguage": "Change language",
@@ -66,6 +69,7 @@
"rechargeReward.accumulatedRecharge": "Accumulated Recharge:",
"rechargeReward.rechargeTitle": "Recharge {amount}",
"rechargeReward.claim": "Claim",
+ "rechargeReward.receive": "Receive",
"rechargeReward.claiming": "Claiming...",
"rechargeReward.claimed": "Claimed",
"rechargeReward.claimSuccess": "Claimed",
@@ -540,6 +544,17 @@
"cp.rewardCoins10000": "Coins*10000",
"cp.rewardCount7": "Count*7days",
"cp.copyright": "The copyright of this activity belongs to Shila",
+ "cp.countdownLabel": "Activity countdown",
+ "cp.huwaaPlazaGiftTitle": "Gifts exchanged between CPs >= 5000 gold will be displayed here",
+ "cp.previousNo1PositionTitle": "The previous number one position",
+ "cp.previousNo1Subtitle": "(Display the last 10 records, sorted in ascending order.)",
+ "cp.rankPodiumLabel": "Top CP ranking",
+ "cp.loading": "Loading...",
+ "cp.empty": "No data yet.",
+ "cp.error": "Unable to load activity data.",
+ "cp.retry": "Retry",
+ "cp.reward": "Reward",
+ "cp.daysLong": "days",
"recharge.pageTitle": "Recharge Center",
"recharge.pageSubtitle": "Choose a coin package and pay.",
"recharge.changeLanguage": "Change language",
@@ -806,5 +821,155 @@
"fami_rank.network_error": "Unable to load the ranking. Please try again.",
"fami_rank.unknown": "Unknown",
"huwaa_rank.page_title": "Huwaa Ranking",
- "huwaa_rank.game": "Game"
+ "huwaa_rank.game": "Game",
+ "fami_vip.back": "Back",
+ "fami_vip.settings": "VIP settings",
+ "fami_vip.help": "VIP help",
+ "fami_vip.levels": "VIP levels",
+ "fami_vip.exclusive_outfit": "Exclusive Outfit",
+ "fami_vip.vip_privileges": "VIP Privileges",
+ "fami_vip.no_outfits": "No exclusive outfits for this level.",
+ "fami_vip.no_privileges": "No privileges for this level.",
+ "fami_vip.outfit": "VIP outfit",
+ "fami_vip.loading": "Loading...",
+ "fami_vip.retry": "Retry",
+ "fami_vip.purchase_region": "VIP purchase",
+ "fami_vip.close": "Close",
+ "fami_vip.help_title": "VIP Help",
+ "fami_vip.help_body": "Benefits and purchase availability are supplied by the VIP service for the selected level.",
+ "fami_vip.settings_title": "VIP Settings",
+ "fami_vip.room_entry_notice": "Room entry notice",
+ "fami_vip.online_notice": "Online global notice",
+ "fami_vip.unlocked": "Unlocked",
+ "fami_vip.locked": "Locked",
+ "fami_vip.days": "{count} Days",
+ "fami_vip.no_expiry": "No expiry",
+ "fami_vip.trial_level": "Trial Level: {level}",
+ "fami_vip.unlocked_level": "Unlocked Level: {level}",
+ "fami_vip.valid_until": "Valid until: {date}",
+ "fami_vip.renew_vip": "Renew VIP",
+ "fami_vip.upgrade_vip": "Upgrade VIP",
+ "fami_vip.activate_vip": "Activate VIP",
+ "fami_vip.unavailable": "Unavailable",
+ "fami_vip.current_vip_higher": "A lower VIP level cannot be purchased while a higher level is active.",
+ "fami_vip.processing": "Processing...",
+ "fami_vip.purchase_success": "Purchase successful.",
+ "fami_vip.purchase_failed": "Purchase failed.",
+ "fami_vip.version_changed": "VIP configuration changed. Please retry.",
+ "fami_vip.load_failed": "Unable to load VIP data.",
+ "fami_vip.settings_saved": "Settings saved.",
+ "fami_vip.settings_failed": "Unable to save settings.",
+ "huwaa_invite.page_title": "Cash Wheel",
+ "huwaa_invite.page_aria": "Huwaa cash wheel invite activity",
+ "huwaa_invite.back": "Back",
+ "huwaa_invite.wallet_aria": "Cash wheel points",
+ "huwaa_invite.points_left": "Points Left To Withdraw:",
+ "huwaa_invite.withdraw": "WITHDRAW TO WALLET",
+ "huwaa_invite.wheel_aria": "Cash wheel",
+ "huwaa_invite.estimated_rewards": "ESTIMATED REWARDS",
+ "huwaa_invite.medium": "Medium",
+ "huwaa_invite.spin": "Spin",
+ "huwaa_invite.invite_now": "Invite Friends Now",
+ "huwaa_invite.tasks_aria": "Invitation tasks",
+ "huwaa_invite.close": "Close",
+ "huwaa_invite.congratulations": "CONGRATULATIONS!",
+ "huwaa_invite.you_won": "You Have Won",
+ "huwaa_invite.awesome": "AWESOME",
+ "huwaa_invite.completed": "COMPLETED",
+ "huwaa_invite.go": "GO",
+ "huwaa_invite.reward": "Reward",
+ "huwaa_invite.login_required": "Please log in first",
+ "huwaa_invite.request_failed": "Request failed",
+ "huwaa_invite.open_app_task": "Open this task in Huwaa",
+ "huwaa_invite.invite_unavailable": "Invitation link is not available yet",
+ "huwaa_invite.copied": "Copied",
+ "huwaa_invite.copy_failed": "Copy failed",
+ "huwaa_invite.open_wallet": "Open the Huwaa wallet to withdraw",
+ "huwaa_invite.task_title": "Use the microphone for 1 min",
+ "huwaa_invite.task_description": "Friend completes biometric authentication",
+ "huwaa_invite.medium_reward": "Medium Reward",
+ "huwaa_invite.points_label": "Points",
+ "huwaa_weekly_star.page_title": "Weekly Star",
+ "huwaa_weekly_star.page_aria": "Huwaa Weekly Star activity",
+ "huwaa_weekly_star.activity_note": "Note: Send the following three gifts to participate in the ranking. Users who rank in the top 3 of the leaderboard will receive rewards.",
+ "huwaa_weekly_star.gifts_aria": "Activity gifts",
+ "huwaa_weekly_star.gift_label": "Activity gift {index}",
+ "huwaa_weekly_star.tabs_aria": "Weekly Star sections",
+ "huwaa_weekly_star.previous_top_three": "Previous Top 3 Records",
+ "huwaa_weekly_star.this_week_rank": "This Week's rank list",
+ "huwaa_weekly_star.reward_description": "Reward Description",
+ "huwaa_weekly_star.loading": "Loading…",
+ "huwaa_weekly_star.no_active_cycle": "No active cycle",
+ "huwaa_weekly_star.no_history": "No previous ranking records.",
+ "huwaa_weekly_star.no_ranking": "No ranking data yet.",
+ "huwaa_weekly_star.no_rewards": "No reward data yet.",
+ "huwaa_weekly_star.no_rank_reward": "No reward data.",
+ "huwaa_weekly_star.login_required": "Please log in to view Weekly Star.",
+ "huwaa_weekly_star.profile_required": "Complete your profile to view Weekly Star.",
+ "huwaa_weekly_star.request_failed": "Unable to load Weekly Star. Please try again.",
+ "huwaa_weekly_star.retry": "Retry",
+ "huwaa_weekly_star.unknown_user": "User",
+ "huwaa_weekly_star.rank_label": "Rank {rank}, {name}, {score}",
+ "huwaa_weekly_star.reward": "Reward",
+ "huwaa_weekly_star.frame": "Frame",
+ "huwaa_weekly_star.reward_days": "{name} ({days} days)",
+ "coin_seller_fami.page_title": "Coins Seller",
+ "coin_seller_fami.page_aria": "Coin seller list",
+ "coin_seller_fami.list_aria": "Available coin sellers",
+ "coin_seller_fami.loading": "Loading coin sellers",
+ "coin_seller_fami.empty": "No coin sellers are available in your region.",
+ "coin_seller_fami.load_failed": "Unable to load coin sellers.",
+ "coin_seller_fami.retry": "Retry",
+ "coin_seller_fami.contact": "Contact",
+ "coin_seller_fami.seller_id": "ID: {id}",
+ "coin_seller_fami.unknown_seller": "Seller",
+ "coin_seller_fami.avatar_label": "{name} avatar",
+ "coin_seller_fami.country_flag": "{country} flag",
+ "coin_seller_fami.contact_seller": "Contact {name}",
+ "coin_seller_fami.contact_unavailable": "Contact unavailable",
+ "fami_week_star.page_title": "Weekly Star",
+ "fami_week_star.page_aria": "Fami Weekly Star activity",
+ "fami_week_star.hero_title": "Weekly star",
+ "fami_week_star.note": "Note: Send the following three gifts to participate in the ranking. Users who rank in the top 3 of the leaderboard will receive rewards.",
+ "fami_week_star.gifts_aria": "Activity gifts",
+ "fami_week_star.tabs_aria": "Weekly Star sections",
+ "fami_week_star.tab_history": "Previous Top 3 Records",
+ "fami_week_star.tab_weekly": "This Week's rank list",
+ "fami_week_star.tab_rewards": "Reward Description",
+ "fami_week_star.loading": "Loading...",
+ "fami_week_star.load_failed": "Unable to load rankings.",
+ "fami_week_star.unknown": "Unknown",
+ "fami_week_star.rank_aria": "Rank {rank}",
+ "fami_week_star.entry_aria": "Rank {rank}, {name}, {score}",
+ "fami_week_star.empty_weekly": "No ranking data yet.",
+ "fami_week_star.empty_history": "No previous ranking records.",
+ "fami_week_star.gift": "Gift",
+ "fami_week_star.coins": "Coins",
+ "fami_week_star.reward": "Reward",
+ "fami_week_star.reward_days": "{name} ({days} days)",
+ "fami_week_star.top_rewards_aria": "Top {rank} rewards",
+ "fami_week_star.activity_load_failed": "Unable to load Weekly Star.",
+ "fami_week_star.no_active_cycle": "No active cycle.",
+ "fami_week_star.empty_rewards": "No reward data yet.",
+ "fami_week_star.mock_reward_name": "Frame",
+ "cp.disabled": "This activity is not available.",
+ "cp.unavailable": "No data source is available yet.",
+ "cp.previousWeekTopTitle": "Previous Top 3",
+ "cp.previousWeekTopSubtitle": "(Top CPs from the previous completed week.)",
+ "cp.rankPlace": "Rank",
+ "cp.rewardTierGold": "First-place rewards",
+ "cp.rewardTierSilver": "Second-place rewards",
+ "cp.rewardTierBronze": "Third-place rewards",
+ "cp.rewardAvatarFrame": "Avatar frame",
+ "cp.rewardProfileCard": "Profile card",
+ "cp.rewardCoins": "Coins",
+ "cp.rewardVehicle": "Vehicle",
+ "cp.rewardChatBubble": "Chat bubble",
+ "cp.rewardBadge": "Badge",
+ "cp.rewardFloatingScreen": "Floating screen",
+ "cp.rewardGift": "Gift",
+ "cp.rewardMicSeatIcon": "Mic seat icon",
+ "cp.rewardMicSeatAnimation": "Mic seat animation",
+ "cp.rewardEmojiPack": "Emoji pack",
+ "cp.rewardVipTrialCard": "VIP trial card"
}
diff --git a/common/locales/es.json b/common/locales/es.json
index 231d40b..ad86fc3 100644
--- a/common/locales/es.json
+++ b/common/locales/es.json
@@ -55,6 +55,9 @@
"roomReward.idPrefix": "ID:",
"roomReward.roomOwner": "Anfitrión de la sala",
"roomReward.changeLanguage": "Cambiar idioma",
+ "roomReward.loading": "Cargando...",
+ "roomReward.empty": "Aún no hay niveles de recompensa",
+ "roomReward.loadFailed": "Error al cargar",
"rechargeReward.pageTitle": "Recompensa de recarga acumulada",
"rechargeReward.pageLabel": "Actividad de recompensa de recarga acumulada",
"rechargeReward.changeLanguage": "Cambiar idioma",
@@ -66,6 +69,7 @@
"rechargeReward.accumulatedRecharge": "Recarga acumulada:",
"rechargeReward.rechargeTitle": "Recarga {amount}",
"rechargeReward.claim": "Reclamar",
+ "rechargeReward.receive": "Recibir",
"rechargeReward.claiming": "Reclamando...",
"rechargeReward.claimed": "Reclamado",
"rechargeReward.claimSuccess": "Reclamado",
@@ -534,6 +538,17 @@
"cp.rewardCoins10000": "Monedas*10000",
"cp.rewardCount7": "Contador*7 días",
"cp.copyright": "Los derechos de esta actividad pertenecen a Shila",
+ "cp.countdownLabel": "Cuenta regresiva de la actividad",
+ "cp.huwaaPlazaGiftTitle": "Aquí se mostrarán los regalos intercambiados entre parejas CP por un valor de 5000 monedas de oro o más",
+ "cp.previousNo1PositionTitle": "Registros anteriores del primer puesto",
+ "cp.previousNo1Subtitle": "(Muestra los últimos 10 registros ordenados de forma ascendente.)",
+ "cp.rankPodiumLabel": "Clasificación de las mejores parejas CP",
+ "cp.loading": "Cargando...",
+ "cp.empty": "Aún no hay datos.",
+ "cp.error": "No se pudieron cargar los datos de la actividad.",
+ "cp.retry": "Reintentar",
+ "cp.reward": "Recompensa",
+ "cp.daysLong": "días",
"recharge.pageTitle": "Recharge Center",
"recharge.pageSubtitle": "Elige un paquete de monedas y completa el pago.",
"recharge.changeLanguage": "Cambiar idioma",
@@ -800,5 +815,155 @@
"fami_rank.network_error": "No se pudo cargar la clasificación. Inténtalo de nuevo.",
"fami_rank.unknown": "Desconocido",
"huwaa_rank.page_title": "Clasificación Huwaa",
- "huwaa_rank.game": "Juego"
+ "huwaa_rank.game": "Juego",
+ "fami_vip.back": "Atrás",
+ "fami_vip.settings": "Ajustes VIP",
+ "fami_vip.help": "Ayuda VIP",
+ "fami_vip.levels": "Niveles VIP",
+ "fami_vip.exclusive_outfit": "Estilo exclusivo",
+ "fami_vip.vip_privileges": "Privilegios VIP",
+ "fami_vip.no_outfits": "No hay estilos exclusivos para este nivel.",
+ "fami_vip.no_privileges": "No hay privilegios para este nivel.",
+ "fami_vip.outfit": "Estilo VIP",
+ "fami_vip.loading": "Cargando...",
+ "fami_vip.retry": "Reintentar",
+ "fami_vip.purchase_region": "Comprar VIP",
+ "fami_vip.close": "Cerrar",
+ "fami_vip.help_title": "Ayuda VIP",
+ "fami_vip.help_body": "Los privilegios y la disponibilidad de compra del nivel seleccionado proceden del servicio VIP.",
+ "fami_vip.settings_title": "Ajustes VIP",
+ "fami_vip.room_entry_notice": "Aviso al entrar en la sala",
+ "fami_vip.online_notice": "Aviso global al conectarse",
+ "fami_vip.unlocked": "Desbloqueado",
+ "fami_vip.locked": "Bloqueado",
+ "fami_vip.days": "{count} días",
+ "fami_vip.no_expiry": "Sin caducidad",
+ "fami_vip.trial_level": "Nivel de prueba: {level}",
+ "fami_vip.unlocked_level": "Nivel desbloqueado: {level}",
+ "fami_vip.valid_until": "Válido hasta: {date}",
+ "fami_vip.renew_vip": "Renovar VIP",
+ "fami_vip.upgrade_vip": "Mejorar VIP",
+ "fami_vip.activate_vip": "Activar VIP",
+ "fami_vip.unavailable": "No disponible",
+ "fami_vip.current_vip_higher": "No se puede comprar un nivel VIP inferior mientras haya uno superior activo.",
+ "fami_vip.processing": "Procesando...",
+ "fami_vip.purchase_success": "Compra realizada.",
+ "fami_vip.purchase_failed": "Error en la compra.",
+ "fami_vip.version_changed": "La configuración VIP cambió. Reinténtalo.",
+ "fami_vip.load_failed": "No se pudieron cargar los datos VIP.",
+ "fami_vip.settings_saved": "Ajustes guardados.",
+ "fami_vip.settings_failed": "No se pudieron guardar los ajustes.",
+ "huwaa_invite.page_title": "Ruleta de efectivo",
+ "huwaa_invite.page_aria": "Actividad de invitación de la ruleta de Huwaa",
+ "huwaa_invite.back": "Volver",
+ "huwaa_invite.wallet_aria": "Puntos de la ruleta",
+ "huwaa_invite.points_left": "Puntos restantes para retirar:",
+ "huwaa_invite.withdraw": "RETIRAR A LA CARTERA",
+ "huwaa_invite.wheel_aria": "Ruleta de efectivo",
+ "huwaa_invite.estimated_rewards": "RECOMPENSAS ESTIMADAS",
+ "huwaa_invite.medium": "Medio",
+ "huwaa_invite.spin": "Girar",
+ "huwaa_invite.invite_now": "Invita amigos ahora",
+ "huwaa_invite.tasks_aria": "Tareas de invitación",
+ "huwaa_invite.close": "Cerrar",
+ "huwaa_invite.congratulations": "¡FELICIDADES!",
+ "huwaa_invite.you_won": "Has ganado",
+ "huwaa_invite.awesome": "GENIAL",
+ "huwaa_invite.completed": "COMPLETADO",
+ "huwaa_invite.go": "IR",
+ "huwaa_invite.reward": "Recompensa",
+ "huwaa_invite.login_required": "Inicia sesión primero",
+ "huwaa_invite.request_failed": "Error en la solicitud",
+ "huwaa_invite.open_app_task": "Abre esta tarea en Huwaa",
+ "huwaa_invite.invite_unavailable": "El enlace de invitación aún no está disponible",
+ "huwaa_invite.copied": "Copiado",
+ "huwaa_invite.copy_failed": "Error al copiar",
+ "huwaa_invite.open_wallet": "Abre la cartera de Huwaa para retirar",
+ "huwaa_invite.task_title": "Usa el micrófono durante 1 min",
+ "huwaa_invite.task_description": "El amigo completa la autenticación biométrica",
+ "huwaa_invite.medium_reward": "Recompensa media",
+ "huwaa_invite.points_label": "Puntos",
+ "huwaa_weekly_star.page_title": "Estrella semanal",
+ "huwaa_weekly_star.page_aria": "Actividad Estrella semanal de Huwaa",
+ "huwaa_weekly_star.activity_note": "Nota: Envía los tres regalos siguientes para participar en la clasificación. Los usuarios que queden entre los 3 primeros recibirán recompensas.",
+ "huwaa_weekly_star.gifts_aria": "Regalos de la actividad",
+ "huwaa_weekly_star.gift_label": "Regalo de actividad {index}",
+ "huwaa_weekly_star.tabs_aria": "Secciones de Estrella semanal",
+ "huwaa_weekly_star.previous_top_three": "Registros anteriores del Top 3",
+ "huwaa_weekly_star.this_week_rank": "Clasificación de esta semana",
+ "huwaa_weekly_star.reward_description": "Descripción de recompensas",
+ "huwaa_weekly_star.loading": "Cargando…",
+ "huwaa_weekly_star.no_active_cycle": "No hay un ciclo activo",
+ "huwaa_weekly_star.no_history": "No hay registros de clasificación anteriores.",
+ "huwaa_weekly_star.no_ranking": "Aún no hay datos de clasificación.",
+ "huwaa_weekly_star.no_rewards": "Aún no hay datos de recompensas.",
+ "huwaa_weekly_star.no_rank_reward": "No hay datos de recompensa.",
+ "huwaa_weekly_star.login_required": "Inicia sesión para ver Estrella semanal.",
+ "huwaa_weekly_star.profile_required": "Completa tu perfil para ver Estrella semanal.",
+ "huwaa_weekly_star.request_failed": "No se pudo cargar Estrella semanal. Inténtalo de nuevo.",
+ "huwaa_weekly_star.retry": "Reintentar",
+ "huwaa_weekly_star.unknown_user": "Usuario",
+ "huwaa_weekly_star.rank_label": "Puesto {rank}, {name}, {score}",
+ "huwaa_weekly_star.reward": "Recompensa",
+ "huwaa_weekly_star.frame": "Marco",
+ "huwaa_weekly_star.reward_days": "{name} ({days} días)",
+ "coin_seller_fami.page_title": "Vendedores de monedas",
+ "coin_seller_fami.page_aria": "Lista de vendedores de monedas",
+ "coin_seller_fami.list_aria": "Vendedores de monedas disponibles",
+ "coin_seller_fami.loading": "Cargando vendedores de monedas",
+ "coin_seller_fami.empty": "No hay vendedores de monedas disponibles en tu región.",
+ "coin_seller_fami.load_failed": "No se pudieron cargar los vendedores.",
+ "coin_seller_fami.retry": "Reintentar",
+ "coin_seller_fami.contact": "Contactar",
+ "coin_seller_fami.seller_id": "ID: {id}",
+ "coin_seller_fami.unknown_seller": "Vendedor",
+ "coin_seller_fami.avatar_label": "Avatar de {name}",
+ "coin_seller_fami.country_flag": "Bandera de {country}",
+ "coin_seller_fami.contact_seller": "Contactar a {name}",
+ "coin_seller_fami.contact_unavailable": "Contacto no disponible",
+ "fami_week_star.page_title": "Estrella semanal",
+ "fami_week_star.page_aria": "Actividad Estrella semanal de Fami",
+ "fami_week_star.hero_title": "Estrella semanal",
+ "fami_week_star.note": "Nota: Envía los siguientes tres regalos para participar en la clasificación. Los usuarios que estén entre los 3 primeros de la tabla recibirán recompensas.",
+ "fami_week_star.gifts_aria": "Regalos de la actividad",
+ "fami_week_star.tabs_aria": "Secciones de Estrella semanal",
+ "fami_week_star.tab_history": "Registros anteriores del Top 3",
+ "fami_week_star.tab_weekly": "Clasificación de esta semana",
+ "fami_week_star.tab_rewards": "Descripción de recompensas",
+ "fami_week_star.loading": "Cargando...",
+ "fami_week_star.load_failed": "No se pudo cargar la clasificación.",
+ "fami_week_star.unknown": "Desconocido",
+ "fami_week_star.rank_aria": "Puesto {rank}",
+ "fami_week_star.entry_aria": "Puesto {rank}, {name}, {score}",
+ "fami_week_star.empty_weekly": "Aún no hay datos de clasificación.",
+ "fami_week_star.empty_history": "No hay registros de clasificación anteriores.",
+ "fami_week_star.gift": "Regalo",
+ "fami_week_star.coins": "Monedas",
+ "fami_week_star.reward": "Recompensa",
+ "fami_week_star.reward_days": "{name} ({days} días)",
+ "fami_week_star.top_rewards_aria": "Recompensas del puesto {rank}",
+ "fami_week_star.activity_load_failed": "No se pudo cargar Estrella semanal.",
+ "fami_week_star.no_active_cycle": "No hay un ciclo activo.",
+ "fami_week_star.empty_rewards": "Aún no hay datos de recompensas.",
+ "fami_week_star.mock_reward_name": "Marco",
+ "cp.disabled": "Esta actividad no está disponible.",
+ "cp.unavailable": "La fuente de datos aún no está disponible.",
+ "cp.previousWeekTopTitle": "Top 3 anterior",
+ "cp.previousWeekTopSubtitle": "(Mejores parejas CP de la semana completada anterior.)",
+ "cp.rankPlace": "Puesto",
+ "cp.rewardTierGold": "Recompensas del primer puesto",
+ "cp.rewardTierSilver": "Recompensas del segundo puesto",
+ "cp.rewardTierBronze": "Recompensas del tercer puesto",
+ "cp.rewardAvatarFrame": "Marco de avatar",
+ "cp.rewardProfileCard": "Tarjeta de perfil",
+ "cp.rewardCoins": "Monedas",
+ "cp.rewardVehicle": "Vehículo",
+ "cp.rewardChatBubble": "Burbuja de chat",
+ "cp.rewardBadge": "Insignia",
+ "cp.rewardFloatingScreen": "Pantalla flotante",
+ "cp.rewardGift": "Regalo",
+ "cp.rewardMicSeatIcon": "Icono de asiento de micrófono",
+ "cp.rewardMicSeatAnimation": "Animación de asiento de micrófono",
+ "cp.rewardEmojiPack": "Paquete de emojis",
+ "cp.rewardVipTrialCard": "Tarjeta de prueba VIP"
}
diff --git a/common/locales/id.json b/common/locales/id.json
index a6dcc5a..c6b5ca3 100644
--- a/common/locales/id.json
+++ b/common/locales/id.json
@@ -22,6 +22,9 @@
"roomReward.idPrefix": "ID:",
"roomReward.roomOwner": "Pemilik room",
"roomReward.changeLanguage": "Ganti bahasa",
+ "roomReward.loading": "Memuat...",
+ "roomReward.empty": "Belum ada level hadiah",
+ "roomReward.loadFailed": "Gagal memuat",
"rechargeReward.pageTitle": "Hadiah Recharge Akumulatif",
"rechargeReward.pageLabel": "Event Hadiah Recharge Akumulatif",
"rechargeReward.changeLanguage": "Ganti bahasa",
@@ -33,6 +36,7 @@
"rechargeReward.accumulatedRecharge": "Recharge akumulatif:",
"rechargeReward.rechargeTitle": "Recharge {amount}",
"rechargeReward.claim": "Klaim",
+ "rechargeReward.receive": "Terima",
"rechargeReward.claiming": "Mengklaim...",
"rechargeReward.claimed": "Diklaim",
"rechargeReward.claimSuccess": "Diklaim",
@@ -692,5 +696,86 @@
"manager_center.send_badge": "Kirim Lencana",
"manager_center.send_vip": "Kirim VIP",
"manager_center.user_level": "Level Pengguna",
- "manager_center.transfer_user_country": "Pindahkan Negara Pengguna"
+ "manager_center.transfer_user_country": "Pindahkan Negara Pengguna",
+ "fami_vip.back": "Kembali",
+ "fami_vip.settings": "Pengaturan VIP",
+ "fami_vip.help": "Bantuan VIP",
+ "fami_vip.levels": "Level VIP",
+ "fami_vip.exclusive_outfit": "Tampilan Eksklusif",
+ "fami_vip.vip_privileges": "Hak Istimewa VIP",
+ "fami_vip.no_outfits": "Tidak ada tampilan eksklusif untuk level ini.",
+ "fami_vip.no_privileges": "Tidak ada hak istimewa untuk level ini.",
+ "fami_vip.outfit": "Tampilan VIP",
+ "fami_vip.loading": "Memuat...",
+ "fami_vip.retry": "Coba lagi",
+ "fami_vip.purchase_region": "Beli VIP",
+ "fami_vip.close": "Tutup",
+ "fami_vip.help_title": "Bantuan VIP",
+ "fami_vip.help_body": "Hak istimewa dan ketersediaan pembelian untuk level yang dipilih diberikan oleh layanan VIP.",
+ "fami_vip.settings_title": "Pengaturan VIP",
+ "fami_vip.room_entry_notice": "Notifikasi masuk room",
+ "fami_vip.online_notice": "Notifikasi online global",
+ "fami_vip.unlocked": "Terbuka",
+ "fami_vip.locked": "Terkunci",
+ "fami_vip.days": "{count} Hari",
+ "fami_vip.no_expiry": "Tanpa kedaluwarsa",
+ "fami_vip.trial_level": "Level Uji Coba: {level}",
+ "fami_vip.unlocked_level": "Level Terbuka: {level}",
+ "fami_vip.valid_until": "Berlaku hingga: {date}",
+ "fami_vip.renew_vip": "Perpanjang VIP",
+ "fami_vip.upgrade_vip": "Tingkatkan VIP",
+ "fami_vip.activate_vip": "Aktifkan VIP",
+ "fami_vip.unavailable": "Tidak tersedia",
+ "fami_vip.current_vip_higher": "Level VIP yang lebih rendah tidak dapat dibeli saat level yang lebih tinggi aktif.",
+ "fami_vip.processing": "Memproses...",
+ "fami_vip.purchase_success": "Pembelian berhasil.",
+ "fami_vip.purchase_failed": "Pembelian gagal.",
+ "fami_vip.version_changed": "Konfigurasi VIP berubah. Coba lagi.",
+ "fami_vip.load_failed": "Data VIP tidak dapat dimuat.",
+ "fami_vip.settings_saved": "Pengaturan disimpan.",
+ "fami_vip.settings_failed": "Pengaturan tidak dapat disimpan.",
+ "huwaa_invite.page_title": "Roda Tunai",
+ "huwaa_invite.page_aria": "Aktivitas undangan roda tunai Huwaa",
+ "huwaa_invite.back": "Kembali",
+ "huwaa_invite.wallet_aria": "Poin roda tunai",
+ "huwaa_invite.points_left": "Poin tersisa untuk ditarik:",
+ "huwaa_invite.withdraw": "TARIK KE DOMPET",
+ "huwaa_invite.wheel_aria": "Roda tunai",
+ "huwaa_invite.estimated_rewards": "PERKIRAAN HADIAH",
+ "huwaa_invite.medium": "Sedang",
+ "huwaa_invite.spin": "Putar",
+ "huwaa_invite.invite_now": "Undang Teman Sekarang",
+ "huwaa_invite.tasks_aria": "Tugas undangan",
+ "huwaa_invite.close": "Tutup",
+ "huwaa_invite.congratulations": "SELAMAT!",
+ "huwaa_invite.you_won": "Kamu menang",
+ "huwaa_invite.awesome": "KEREN",
+ "huwaa_invite.completed": "SELESAI",
+ "huwaa_invite.go": "BUKA",
+ "huwaa_invite.reward": "Hadiah",
+ "huwaa_invite.login_required": "Silakan masuk terlebih dahulu",
+ "huwaa_invite.request_failed": "Permintaan gagal",
+ "huwaa_invite.open_app_task": "Buka tugas ini di Huwaa",
+ "huwaa_invite.invite_unavailable": "Tautan undangan belum tersedia",
+ "huwaa_invite.copied": "Disalin",
+ "huwaa_invite.copy_failed": "Gagal menyalin",
+ "huwaa_invite.open_wallet": "Buka dompet Huwaa untuk menarik",
+ "huwaa_invite.task_title": "Gunakan mikrofon selama 1 menit",
+ "huwaa_invite.task_description": "Teman menyelesaikan autentikasi biometrik",
+ "huwaa_invite.medium_reward": "Hadiah Sedang",
+ "huwaa_invite.points_label": "Poin",
+ "coin_seller_fami.page_title": "Penjual Koin",
+ "coin_seller_fami.page_aria": "Daftar penjual koin",
+ "coin_seller_fami.list_aria": "Penjual koin yang tersedia",
+ "coin_seller_fami.loading": "Memuat penjual koin",
+ "coin_seller_fami.empty": "Tidak ada penjual koin di wilayah Anda.",
+ "coin_seller_fami.load_failed": "Penjual koin tidak dapat dimuat.",
+ "coin_seller_fami.retry": "Coba Lagi",
+ "coin_seller_fami.contact": "Hubungi",
+ "coin_seller_fami.seller_id": "ID: {id}",
+ "coin_seller_fami.unknown_seller": "Penjual",
+ "coin_seller_fami.avatar_label": "Avatar {name}",
+ "coin_seller_fami.country_flag": "Bendera {country}",
+ "coin_seller_fami.contact_seller": "Hubungi {name}",
+ "coin_seller_fami.contact_unavailable": "Kontak tidak tersedia"
}
diff --git a/common/locales/tr.json b/common/locales/tr.json
index b54e18b..7df8138 100644
--- a/common/locales/tr.json
+++ b/common/locales/tr.json
@@ -22,6 +22,9 @@
"roomReward.idPrefix": "ID:",
"roomReward.roomOwner": "Oda sahibi",
"roomReward.changeLanguage": "Dili değiştir",
+ "roomReward.loading": "Yükleniyor...",
+ "roomReward.empty": "Henüz ödül kademesi yok",
+ "roomReward.loadFailed": "Yükleme başarısız",
"rechargeReward.pageTitle": "Kümülatif Yükleme Ödülü",
"rechargeReward.pageLabel": "Kümülatif Yükleme Ödülü etkinliği",
"rechargeReward.changeLanguage": "Dili değiştir",
@@ -33,6 +36,7 @@
"rechargeReward.accumulatedRecharge": "Toplam yükleme:",
"rechargeReward.rechargeTitle": "{amount} yükle",
"rechargeReward.claim": "Al",
+ "rechargeReward.receive": "Al",
"rechargeReward.claiming": "Alınıyor...",
"rechargeReward.claimed": "Alındı",
"rechargeReward.claimSuccess": "Alındı",
@@ -501,6 +505,17 @@
"cp.rewardCoins10000": "Coin*10000",
"cp.rewardCount7": "Sayaç*7 gün",
"cp.copyright": "Bu etkinliğin telif hakkı Shila'ya aittir",
+ "cp.countdownLabel": "Etkinlik geri sayımı",
+ "cp.huwaaPlazaGiftTitle": "CP'ler arasında takas edilen 5000 veya daha fazla altın değerindeki hediyeler burada gösterilir",
+ "cp.previousNo1PositionTitle": "Önceki birincilik kayıtları",
+ "cp.previousNo1Subtitle": "(Son 10 kayıt artan sırada gösterilir.)",
+ "cp.rankPodiumLabel": "En iyi CP sıralaması",
+ "cp.loading": "Yükleniyor...",
+ "cp.empty": "Henüz veri yok.",
+ "cp.error": "Etkinlik verileri yüklenemedi.",
+ "cp.retry": "Tekrar dene",
+ "cp.reward": "Ödül",
+ "cp.daysLong": "gün",
"recharge.pageTitle": "Recharge Center",
"recharge.pageSubtitle": "Coin paketi seçin ve ödemeyi tamamlayın.",
"recharge.changeLanguage": "Dili değiştir",
@@ -767,5 +782,155 @@
"fami_rank.network_error": "Sıralama yüklenemedi. Lütfen tekrar deneyin.",
"fami_rank.unknown": "Bilinmiyor",
"huwaa_rank.page_title": "Huwaa Sıralaması",
- "huwaa_rank.game": "Oyun"
+ "huwaa_rank.game": "Oyun",
+ "fami_vip.back": "Geri",
+ "fami_vip.settings": "VIP ayarları",
+ "fami_vip.help": "VIP yardımı",
+ "fami_vip.levels": "VIP seviyeleri",
+ "fami_vip.exclusive_outfit": "Özel Görünüm",
+ "fami_vip.vip_privileges": "VIP Ayrıcalıkları",
+ "fami_vip.no_outfits": "Bu seviyede özel görünüm yok.",
+ "fami_vip.no_privileges": "Bu seviyede ayrıcalık yok.",
+ "fami_vip.outfit": "VIP görünümü",
+ "fami_vip.loading": "Yükleniyor...",
+ "fami_vip.retry": "Tekrar dene",
+ "fami_vip.purchase_region": "VIP satın al",
+ "fami_vip.close": "Kapat",
+ "fami_vip.help_title": "VIP Yardımı",
+ "fami_vip.help_body": "Seçilen seviyenin ayrıcalıkları ve satın alma durumu VIP hizmeti tarafından sağlanır.",
+ "fami_vip.settings_title": "VIP Ayarları",
+ "fami_vip.room_entry_notice": "Odaya giriş bildirimi",
+ "fami_vip.online_notice": "Çevrim içi genel bildirim",
+ "fami_vip.unlocked": "Açık",
+ "fami_vip.locked": "Kilitli",
+ "fami_vip.days": "{count} Gün",
+ "fami_vip.no_expiry": "Süresiz",
+ "fami_vip.trial_level": "Deneme Seviyesi: {level}",
+ "fami_vip.unlocked_level": "Açık Seviye: {level}",
+ "fami_vip.valid_until": "Geçerlilik: {date}",
+ "fami_vip.renew_vip": "VIP Yenile",
+ "fami_vip.upgrade_vip": "VIP Yükselt",
+ "fami_vip.activate_vip": "VIP Etkinleştir",
+ "fami_vip.unavailable": "Kullanılamıyor",
+ "fami_vip.current_vip_higher": "Daha yüksek bir VIP seviyesi etkinken daha düşük seviye satın alınamaz.",
+ "fami_vip.processing": "İşleniyor...",
+ "fami_vip.purchase_success": "Satın alma başarılı.",
+ "fami_vip.purchase_failed": "Satın alma başarısız.",
+ "fami_vip.version_changed": "VIP yapılandırması değişti. Tekrar deneyin.",
+ "fami_vip.load_failed": "VIP verileri yüklenemedi.",
+ "fami_vip.settings_saved": "Ayarlar kaydedildi.",
+ "fami_vip.settings_failed": "Ayarlar kaydedilemedi.",
+ "huwaa_invite.page_title": "Nakit Çarkı",
+ "huwaa_invite.page_aria": "Huwaa nakit çarkı davet etkinliği",
+ "huwaa_invite.back": "Geri",
+ "huwaa_invite.wallet_aria": "Nakit çarkı puanları",
+ "huwaa_invite.points_left": "Çekim İçin Kalan Puan:",
+ "huwaa_invite.withdraw": "CÜZDANA ÇEK",
+ "huwaa_invite.wheel_aria": "Nakit çarkı",
+ "huwaa_invite.estimated_rewards": "TAHMİNİ ÖDÜLLER",
+ "huwaa_invite.medium": "Orta",
+ "huwaa_invite.spin": "Çevir",
+ "huwaa_invite.invite_now": "Arkadaşlarını Şimdi Davet Et",
+ "huwaa_invite.tasks_aria": "Davet görevleri",
+ "huwaa_invite.close": "Kapat",
+ "huwaa_invite.congratulations": "TEBRİKLER!",
+ "huwaa_invite.you_won": "Kazandın",
+ "huwaa_invite.awesome": "HARİKA",
+ "huwaa_invite.completed": "TAMAMLANDI",
+ "huwaa_invite.go": "GİT",
+ "huwaa_invite.reward": "Ödül",
+ "huwaa_invite.login_required": "Lütfen önce giriş yapın",
+ "huwaa_invite.request_failed": "İstek başarısız",
+ "huwaa_invite.open_app_task": "Bu görevi Huwaa'da aç",
+ "huwaa_invite.invite_unavailable": "Davet bağlantısı henüz mevcut değil",
+ "huwaa_invite.copied": "Kopyalandı",
+ "huwaa_invite.copy_failed": "Kopyalama başarısız",
+ "huwaa_invite.open_wallet": "Çekmek için Huwaa cüzdanını aç",
+ "huwaa_invite.task_title": "Mikrofonu 1 dakika kullan",
+ "huwaa_invite.task_description": "Arkadaş biyometrik doğrulamayı tamamlar",
+ "huwaa_invite.medium_reward": "Orta Ödül",
+ "huwaa_invite.points_label": "Puan",
+ "huwaa_weekly_star.page_title": "Haftanın Yıldızı",
+ "huwaa_weekly_star.page_aria": "Huwaa Haftanın Yıldızı etkinliği",
+ "huwaa_weekly_star.activity_note": "Not: Sıralamaya katılmak için aşağıdaki üç hediyeyi gönderin. Liderlik tablosunda ilk 3'e giren kullanıcılar ödül kazanır.",
+ "huwaa_weekly_star.gifts_aria": "Etkinlik hediyeleri",
+ "huwaa_weekly_star.gift_label": "Etkinlik hediyesi {index}",
+ "huwaa_weekly_star.tabs_aria": "Haftanın Yıldızı bölümleri",
+ "huwaa_weekly_star.previous_top_three": "Önceki İlk 3 Kayıtları",
+ "huwaa_weekly_star.this_week_rank": "Bu Haftanın Sıralaması",
+ "huwaa_weekly_star.reward_description": "Ödül Açıklaması",
+ "huwaa_weekly_star.loading": "Yükleniyor…",
+ "huwaa_weekly_star.no_active_cycle": "Aktif dönem yok",
+ "huwaa_weekly_star.no_history": "Önceki sıralama kaydı yok.",
+ "huwaa_weekly_star.no_ranking": "Henüz sıralama verisi yok.",
+ "huwaa_weekly_star.no_rewards": "Henüz ödül verisi yok.",
+ "huwaa_weekly_star.no_rank_reward": "Ödül verisi yok.",
+ "huwaa_weekly_star.login_required": "Haftanın Yıldızı'nı görmek için giriş yapın.",
+ "huwaa_weekly_star.profile_required": "Haftanın Yıldızı'nı görmek için profilinizi tamamlayın.",
+ "huwaa_weekly_star.request_failed": "Haftanın Yıldızı yüklenemedi. Tekrar deneyin.",
+ "huwaa_weekly_star.retry": "Tekrar Dene",
+ "huwaa_weekly_star.unknown_user": "Kullanıcı",
+ "huwaa_weekly_star.rank_label": "Sıra {rank}, {name}, {score}",
+ "huwaa_weekly_star.reward": "Ödül",
+ "huwaa_weekly_star.frame": "Çerçeve",
+ "huwaa_weekly_star.reward_days": "{name} ({days} gün)",
+ "coin_seller_fami.page_title": "Coin Satıcıları",
+ "coin_seller_fami.page_aria": "Coin satıcıları listesi",
+ "coin_seller_fami.list_aria": "Mevcut coin satıcıları",
+ "coin_seller_fami.loading": "Coin satıcıları yükleniyor",
+ "coin_seller_fami.empty": "Bölgenizde kullanılabilir coin satıcısı yok.",
+ "coin_seller_fami.load_failed": "Coin satıcıları yüklenemedi.",
+ "coin_seller_fami.retry": "Tekrar Dene",
+ "coin_seller_fami.contact": "İletişim",
+ "coin_seller_fami.seller_id": "ID: {id}",
+ "coin_seller_fami.unknown_seller": "Satıcı",
+ "coin_seller_fami.avatar_label": "{name} avatarı",
+ "coin_seller_fami.country_flag": "{country} bayrağı",
+ "coin_seller_fami.contact_seller": "{name} ile iletişime geç",
+ "coin_seller_fami.contact_unavailable": "İletişim kullanılamıyor",
+ "fami_week_star.page_title": "Haftanın Yıldızı",
+ "fami_week_star.page_aria": "Fami Haftanın Yıldızı etkinliği",
+ "fami_week_star.hero_title": "Haftanın yıldızı",
+ "fami_week_star.note": "Not: Sıralamaya katılmak için aşağıdaki üç hediyeyi gönderin. Liderlik tablosunda ilk 3'e giren kullanıcılar ödül kazanır.",
+ "fami_week_star.gifts_aria": "Etkinlik hediyeleri",
+ "fami_week_star.tabs_aria": "Haftanın Yıldızı bölümleri",
+ "fami_week_star.tab_history": "Önceki İlk 3 Kayıt",
+ "fami_week_star.tab_weekly": "Bu Haftanın Sıralaması",
+ "fami_week_star.tab_rewards": "Ödül Açıklaması",
+ "fami_week_star.loading": "Yükleniyor...",
+ "fami_week_star.load_failed": "Sıralama yüklenemedi.",
+ "fami_week_star.unknown": "Bilinmiyor",
+ "fami_week_star.rank_aria": "Sıra {rank}",
+ "fami_week_star.entry_aria": "Sıra {rank}, {name}, {score}",
+ "fami_week_star.empty_weekly": "Henüz sıralama verisi yok.",
+ "fami_week_star.empty_history": "Önceki sıralama kaydı yok.",
+ "fami_week_star.gift": "Hediye",
+ "fami_week_star.coins": "Jetonlar",
+ "fami_week_star.reward": "Ödül",
+ "fami_week_star.reward_days": "{name} ({days} gün)",
+ "fami_week_star.top_rewards_aria": "İlk {rank} ödülleri",
+ "fami_week_star.activity_load_failed": "Haftanın Yıldızı yüklenemedi.",
+ "fami_week_star.no_active_cycle": "Aktif dönem yok.",
+ "fami_week_star.empty_rewards": "Henüz ödül verisi yok.",
+ "fami_week_star.mock_reward_name": "Çerçeve",
+ "cp.disabled": "Bu etkinlik kullanılamıyor.",
+ "cp.unavailable": "Veri kaynağı henüz kullanılamıyor.",
+ "cp.previousWeekTopTitle": "Önceki İlk 3",
+ "cp.previousWeekTopSubtitle": "(Önceki tamamlanan haftanın en iyi CP çiftleri.)",
+ "cp.rankPlace": "Sıra",
+ "cp.rewardTierGold": "Birincilik ödülleri",
+ "cp.rewardTierSilver": "İkincilik ödülleri",
+ "cp.rewardTierBronze": "Üçüncülük ödülleri",
+ "cp.rewardAvatarFrame": "Avatar çerçevesi",
+ "cp.rewardProfileCard": "Profil kartı",
+ "cp.rewardCoins": "Jeton",
+ "cp.rewardVehicle": "Araç",
+ "cp.rewardChatBubble": "Sohbet balonu",
+ "cp.rewardBadge": "Rozet",
+ "cp.rewardFloatingScreen": "Kayan ekran",
+ "cp.rewardGift": "Hediye",
+ "cp.rewardMicSeatIcon": "Mikrofon koltuğu simgesi",
+ "cp.rewardMicSeatAnimation": "Mikrofon koltuğu animasyonu",
+ "cp.rewardEmojiPack": "Emoji paketi",
+ "cp.rewardVipTrialCard": "VIP deneme kartı"
}
diff --git a/common/locales/zh.json b/common/locales/zh.json
index 15a11bc..1298467 100644
--- a/common/locales/zh.json
+++ b/common/locales/zh.json
@@ -22,6 +22,9 @@
"roomReward.idPrefix": "ID:",
"roomReward.roomOwner": "房主",
"roomReward.changeLanguage": "切换语言",
+ "roomReward.loading": "加载中...",
+ "roomReward.empty": "暂无奖励档位",
+ "roomReward.loadFailed": "加载失败",
"rechargeReward.pageTitle": "累充奖励",
"rechargeReward.pageLabel": "累充奖励活动",
"rechargeReward.changeLanguage": "切换语言",
@@ -33,6 +36,7 @@
"rechargeReward.accumulatedRecharge": "累计充值:",
"rechargeReward.rechargeTitle": "充值 {amount}",
"rechargeReward.claim": "领取",
+ "rechargeReward.receive": "领取",
"rechargeReward.claiming": "领取中...",
"rechargeReward.claimed": "已领取",
"rechargeReward.claimSuccess": "领取成功",
@@ -698,5 +702,86 @@
"manager_center.send_badge": "赠送徽章",
"manager_center.send_vip": "赠送 VIP",
"manager_center.user_level": "用户等级",
- "manager_center.transfer_user_country": "转移用户国家"
+ "manager_center.transfer_user_country": "转移用户国家",
+ "fami_vip.back": "返回",
+ "fami_vip.settings": "VIP 设置",
+ "fami_vip.help": "VIP 帮助",
+ "fami_vip.levels": "VIP 等级",
+ "fami_vip.exclusive_outfit": "专属装扮",
+ "fami_vip.vip_privileges": "VIP 权益",
+ "fami_vip.no_outfits": "该等级暂无专属装扮。",
+ "fami_vip.no_privileges": "该等级暂无权益。",
+ "fami_vip.outfit": "VIP 装扮",
+ "fami_vip.loading": "加载中...",
+ "fami_vip.retry": "重试",
+ "fami_vip.purchase_region": "购买 VIP",
+ "fami_vip.close": "关闭",
+ "fami_vip.help_title": "VIP 帮助",
+ "fami_vip.help_body": "当前等级的权益和购买资格均由 VIP 服务返回。",
+ "fami_vip.settings_title": "VIP 设置",
+ "fami_vip.room_entry_notice": "进房高亮通知",
+ "fami_vip.online_notice": "上线全服通知",
+ "fami_vip.unlocked": "已解锁",
+ "fami_vip.locked": "未解锁",
+ "fami_vip.days": "{count} 天",
+ "fami_vip.no_expiry": "无到期时间",
+ "fami_vip.trial_level": "体验等级:{level}",
+ "fami_vip.unlocked_level": "已解锁等级:{level}",
+ "fami_vip.valid_until": "有效期至:{date}",
+ "fami_vip.renew_vip": "续费 VIP",
+ "fami_vip.upgrade_vip": "升级 VIP",
+ "fami_vip.activate_vip": "开通 VIP",
+ "fami_vip.unavailable": "暂不可用",
+ "fami_vip.current_vip_higher": "高等级 VIP 生效期间不能购买更低等级。",
+ "fami_vip.processing": "处理中...",
+ "fami_vip.purchase_success": "购买成功。",
+ "fami_vip.purchase_failed": "购买失败。",
+ "fami_vip.version_changed": "VIP 配置已更新,请重试。",
+ "fami_vip.load_failed": "VIP 数据加载失败。",
+ "fami_vip.settings_saved": "设置已保存。",
+ "fami_vip.settings_failed": "设置保存失败。",
+ "huwaa_invite.page_title": "现金转盘",
+ "huwaa_invite.page_aria": "Huwaa 现金转盘邀请活动",
+ "huwaa_invite.back": "返回",
+ "huwaa_invite.wallet_aria": "现金转盘积分",
+ "huwaa_invite.points_left": "距离提现还差:",
+ "huwaa_invite.withdraw": "提现到钱包",
+ "huwaa_invite.wheel_aria": "现金转盘",
+ "huwaa_invite.estimated_rewards": "预计奖励",
+ "huwaa_invite.medium": "中等奖励",
+ "huwaa_invite.spin": "抽奖",
+ "huwaa_invite.invite_now": "立即邀请好友",
+ "huwaa_invite.tasks_aria": "邀请任务",
+ "huwaa_invite.close": "关闭",
+ "huwaa_invite.congratulations": "恭喜!",
+ "huwaa_invite.you_won": "你已获得",
+ "huwaa_invite.awesome": "太棒了",
+ "huwaa_invite.completed": "已完成",
+ "huwaa_invite.go": "去完成",
+ "huwaa_invite.reward": "奖励",
+ "huwaa_invite.login_required": "请先登录",
+ "huwaa_invite.request_failed": "请求失败",
+ "huwaa_invite.open_app_task": "请在 Huwaa 中完成该任务",
+ "huwaa_invite.invite_unavailable": "邀请链接暂不可用",
+ "huwaa_invite.copied": "已复制",
+ "huwaa_invite.copy_failed": "复制失败",
+ "huwaa_invite.open_wallet": "请打开 Huwaa 钱包提现",
+ "huwaa_invite.task_title": "使用麦克风 1 分钟",
+ "huwaa_invite.task_description": "好友完成生物识别认证",
+ "huwaa_invite.medium_reward": "中等奖励",
+ "huwaa_invite.points_label": "积分",
+ "coin_seller_fami.page_title": "金币商家",
+ "coin_seller_fami.page_aria": "金币商家列表",
+ "coin_seller_fami.list_aria": "可联系的金币商家",
+ "coin_seller_fami.loading": "正在加载金币商家",
+ "coin_seller_fami.empty": "你所在区域暂无可用金币商家。",
+ "coin_seller_fami.load_failed": "金币商家加载失败。",
+ "coin_seller_fami.retry": "重试",
+ "coin_seller_fami.contact": "联系",
+ "coin_seller_fami.seller_id": "ID:{id}",
+ "coin_seller_fami.unknown_seller": "商家",
+ "coin_seller_fami.avatar_label": "{name}的头像",
+ "coin_seller_fami.country_flag": "{country}国旗",
+ "coin_seller_fami.contact_seller": "联系{name}",
+ "coin_seller_fami.contact_unavailable": "暂时无法联系"
}
diff --git a/common/params.js b/common/params.js
index e49387e..5b3ee67 100644
--- a/common/params.js
+++ b/common/params.js
@@ -38,7 +38,9 @@
function decodeBase64URLJSON(value) {
try {
- var normalized = String(value || '').replace(/-/g, '+').replace(/_/g, '/');
+ var normalized = String(value || '')
+ .replace(/-/g, '+')
+ .replace(/_/g, '/');
while (normalized.length % 4) normalized += '=';
return JSON.parse(window.atob(normalized));
} catch (_) {
@@ -60,6 +62,11 @@
var token = normalizeToken(first(params, TOKEN_KEYS));
var appCode =
first(params, APP_CODE_KEYS) ||
+ // 多产品入口会在 common 脚本执行前声明页面默认租户;它必须优先于历史 localStorage,
+ // 否则用户先访问 Lalu 后再打开无 app_code 的 Fami 深链会把请求错误地切回 Lalu。
+ String(window.HyAppDefaultAppCode || '')
+ .trim()
+ .toLowerCase() ||
window.localStorage.getItem('hyapp_app_code') ||
'lalu';
diff --git a/guild/fami/README.md b/guild/fami/README.md
index 8a9e41b..d1c635f 100644
--- a/guild/fami/README.md
+++ b/guild/fami/README.md
@@ -1,12 +1,17 @@
# Fami guild page entries
-This product will expose four independent static entries:
+This product exposes four independent static entries:
- `host-center/`
- `agency-center/`
- `bd-center/`
- `wallet/`
-Entries are intentionally thin. Shared business logic belongs in `guild/packages//`; Fami-specific configuration and visual overrides belong in `guild/products/fami/`.
+The Figma-restored Center shell, traced layer assets, and shared page runtime live in
+`guild/products/fami/centers/`. Business API contracts remain in
+`guild/packages//`, while Fami product configuration and product locales
+remain in `guild/products/fami/`.
-Do not copy scripts from `gonghui/` wholesale. Reuse existing backend semantics through a feature adapter and keep any Fami response differences additive and explicit.
+`gonghui/` is a separate Lalu product surface and must not be used as the Fami
+runtime route. Reuse backend semantics through the feature adapters and keep any
+Fami response differences additive and explicit.
diff --git a/guild/fami/agency-center/index.html b/guild/fami/agency-center/index.html
index 6178b81..5d9826d 100644
--- a/guild/fami/agency-center/index.html
+++ b/guild/fami/agency-center/index.html
@@ -1,422 +1,325 @@
-
+
- Agency Center
+ Agency
-
+
-
-
-
-