@yyyu-star/arcade-kit 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/social.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * - 接口形状是主线定的(docs/platform-backend.md 里平台写细节):都要登录,写的请求带 `x-arcade: 1`、同源(平台的 CSRF 规矩)。
10
10
  */
11
11
  import { detectPlatform, type PlatformInfo } from './client.js';
12
+ import { applyTheme, type KitTheme } from './theme.js';
12
13
 
13
14
  /** 报位置:在大厅、在玩某个游戏、在某个好友房里(seats 是"3/4"这样的人数) */
14
15
  export interface Presence {
@@ -291,6 +292,8 @@ export interface SocialOptions extends ApiOptions {
291
292
  onGo?(invite: Invite): void;
292
293
  /** 横条的 z-index,默认 10500(SOCIAL_Z.banner:盖得住游戏的弹窗,在"叫朋友来"面板和分享预览之下) */
293
294
  zIndex?: number;
295
+ /** 主题(0.16.0):写到横条根节点上的 `--ak-*` 变量(`handle.openInvite` 打开的面板默认也用它);不传就用页面 CSS 里的变量或默认外观 */
296
+ theme?: KitTheme;
294
297
  /** 冒烟、调试看的流水:`位置 204`、`邀请 弹 <id>`、`邀请 去`、`邀请 不了`、`邀请 过期` */
295
298
  log?(line: string): void;
296
299
  /** 测试用:时钟 */
@@ -344,6 +347,8 @@ class Social implements SocialHandle {
344
347
 
345
348
  openInvite(inv: Omit<InvitePanelOptions, 'fetchImpl' | 'gameName'>): InvitePanel {
346
349
  return openInvite({
350
+ // mountSocial 的主题也给面板用,面板自己传了就以面板的为准
351
+ ...(this.o.theme ? { theme: this.o.theme } : {}),
347
352
  ...inv,
348
353
  ...(this.o.fetchImpl ? { fetchImpl: this.o.fetchImpl } : {}),
349
354
  ...(this.o.gameName ? { gameName: this.o.gameName } : {}),
@@ -411,15 +416,17 @@ class Social implements SocialHandle {
411
416
  'max-width:calc(100vw - 16px)',
412
417
  'box-sizing:border-box',
413
418
  'padding:6px 6px 6px 16px',
414
- 'border-radius:28px',
415
- 'background:rgba(28,28,32,.94)',
416
- 'color:#fff',
419
+ 'border-radius:var(--ak-radius,28px)',
420
+ 'background:var(--ak-bg,rgba(28,28,32,.94))',
421
+ 'color:var(--ak-fg,#fff)',
417
422
  'box-shadow:0 6px 24px rgba(0,0,0,.35)',
418
- 'font:600 16px system-ui,"PingFang SC","Microsoft YaHei",sans-serif',
423
+ 'font:600 16px var(--ak-font,system-ui,"PingFang SC","Microsoft YaHei",sans-serif)',
419
424
  'transform:translate(-50%,-140%)',
420
425
  'opacity:0',
421
426
  'transition:transform .25s ease-out,opacity .25s ease-out',
422
427
  ].join(';');
428
+ // 写在 cssText 之后:cssText 赋值会清掉之前 setProperty 的变量
429
+ applyTheme(el, this.o.theme);
423
430
  const text = document.createElement('span');
424
431
  text.textContent = inviteText(v, this.o.gameName);
425
432
  text.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0';
@@ -428,7 +435,7 @@ class Social implements SocialHandle {
428
435
  b.type = 'button';
429
436
  b.textContent = label;
430
437
  b.dataset['key'] = key;
431
- b.style.cssText = `flex:none;min-width:64px;min-height:44px;padding:0 14px;border-radius:22px;border:0;font:inherit;cursor:pointer;${primary ? 'background:#f5c542;color:#222' : 'background:rgba(255,255,255,.14);color:#fff'}`;
438
+ b.style.cssText = `flex:none;min-width:64px;min-height:44px;padding:0 14px;border-radius:var(--ak-radius-btn,22px);border:0;font:inherit;cursor:pointer;${primary ? 'background:var(--ak-accent,#f5c542);color:var(--ak-accent-fg,#222)' : 'background:var(--ak-soft,rgba(255,255,255,.14));color:var(--ak-fg,#fff)'}`;
432
439
  b.addEventListener('click', fn);
433
440
  return b;
434
441
  };
@@ -499,6 +506,8 @@ export interface InvitePanelOptions extends ApiOptions {
499
506
  gameName?(id: string): string | undefined;
500
507
  /** 默认 10600(SOCIAL_Z.panel:在游戏的弹窗、邀请横条之上,在分享预览之下);游戏的层更高就传更大的 */
501
508
  zIndex?: number;
509
+ /** 主题(0.16.0):写到面板根节点上的 `--ak-*` 变量;不传就用页面 CSS 里的变量或默认外观 */
510
+ theme?: KitTheme;
502
511
  /** 冒烟、调试看的流水:`叫人 打开 <n>`、`叫人 <id> ok|<错误>`、`叫人 复制`、`叫人 关闭` */
503
512
  log?(line: string): void;
504
513
  /** 测试用:时钟 */
@@ -513,19 +522,20 @@ const PANEL_ID = 'arcade-invite-panel';
513
522
  const PANEL_CSS_ID = 'arcade-invite-css';
514
523
  /** 和牌桌互动(table-fun)同一套颜色、圆角、按钮 */
515
524
  const PANEL_CSS = `
516
- .aiv-back{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:8px;box-sizing:border-box;background:rgba(0,0,0,.5);font-family:inherit;-webkit-tap-highlight-color:transparent}
517
- .aiv-box{position:relative;display:flex;flex-direction:column;width:min(420px,calc(100vw - 16px));max-height:calc(100% - 16px);background:rgba(43,29,20,.97);border:2px solid #d4a64a;border-radius:14px;padding:12px;box-sizing:border-box;color:#fff6e0;box-shadow:0 6px 18px rgba(0,0,0,.45);animation:aiv-pop .14s ease-out}
518
- .aiv-title{font-size:17px;font-weight:700;color:#f3d98b;margin:2px 48px 2px 4px}
525
+ .aiv-back{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:8px;box-sizing:border-box;background:var(--ak-overlay,rgba(0,0,0,.5));font-family:var(--ak-font,inherit);-webkit-tap-highlight-color:transparent}
526
+ .aiv-box{position:relative;display:flex;flex-direction:column;width:min(420px,calc(100vw - 16px));max-height:calc(100% - 16px);background:var(--ak-bg,rgba(43,29,20,.97));border:2px solid var(--ak-border,#d4a64a);border-radius:var(--ak-radius,14px);padding:12px;box-sizing:border-box;color:var(--ak-fg,#fff6e0);box-shadow:0 6px 18px rgba(0,0,0,.45);animation:aiv-pop .14s ease-out}
527
+ .aiv-title{font-size:17px;font-weight:700;color:var(--ak-title,#f3d98b);margin:2px 48px 2px 4px}
519
528
  .aiv-sub{font-size:13px;opacity:.75;margin:0 4px 8px}
520
- .aiv-x{position:absolute;right:6px;top:6px;width:44px;height:44px;border:0;border-radius:22px;background:transparent;color:#f3d98b;font:inherit;font-size:22px;cursor:pointer}
529
+ .aiv-x{position:absolute;right:6px;top:6px;width:44px;height:44px;border:0;border-radius:var(--ak-radius-btn,22px);background:transparent;color:var(--ak-title,#f3d98b);font:inherit;font-size:22px;cursor:pointer}
521
530
  .aiv-list{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:6px}
522
- .aiv-row{display:flex;align-items:center;gap:8px;padding:6px 6px 6px 10px;border-radius:10px;background:#4a3220}
531
+ .aiv-row{display:flex;align-items:center;gap:8px;padding:6px 6px 6px 10px;border-radius:var(--ak-radius-btn,10px);background:var(--ak-soft,#4a3220)}
523
532
  .aiv-dot{flex:none;width:10px;height:10px;border-radius:50%;background:#6a6a6a}.aiv-dot.aiv-on{background:#6fd36f}
524
533
  .aiv-who{flex:1 1 auto;min-width:0}
525
534
  .aiv-name{font-size:15px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
526
535
  .aiv-where{font-size:12px;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
527
- .aiv-chip{flex:none;min-width:84px;min-height:44px;border:1px solid rgba(212,166,74,.6);border-radius:22px;background:#7a2a1e;color:#fff6e0;font:inherit;font-size:14px;font-weight:700;padding:0 12px;cursor:pointer}
528
- .aiv-chip:disabled{background:#3a3a3a;cursor:default;opacity:.8}.aiv-chip.aiv-soft{background:#4a3220}
536
+ .aiv-chip{flex:none;min-width:84px;min-height:44px;border:1px solid var(--ak-border,rgba(212,166,74,.6));border-radius:var(--ak-radius-btn,22px);background:var(--ak-accent,#7a2a1e);color:var(--ak-accent-fg,#fff6e0);font:inherit;font-size:14px;font-weight:700;padding:0 12px;cursor:pointer}
537
+ .aiv-chip:disabled,.aiv-chip.aiv-soft{color:var(--ak-fg,#fff6e0)}
538
+ .aiv-chip:disabled{background:#3a3a3a;cursor:default;opacity:.8}.aiv-chip.aiv-soft{background:var(--ak-soft,#4a3220)}
529
539
  .aiv-note{font-size:13px;text-align:center;opacity:.85;padding:10px 4px;min-height:1em}
530
540
  .aiv-err{color:#ffb0a0}
531
541
  .aiv-foot{display:flex;justify-content:center;padding-top:10px}
@@ -559,6 +569,7 @@ export function openInvite(o: InvitePanelOptions): InvitePanel {
559
569
  const back = el('div', 'aiv-back');
560
570
  back.id = PANEL_ID;
561
571
  back.style.zIndex = String(o.zIndex ?? SOCIAL_Z.panel);
572
+ applyTheme(back, o.theme);
562
573
  const box = el('div', 'aiv-box');
563
574
  box.setAttribute('role', 'dialog');
564
575
  box.setAttribute('aria-label', '叫朋友来');
package/src/table-fun.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * - 公平:人人一样、免费;只是画面和声音,不停计时、不改规则;电脑的出牌不读这些。电脑的反应只在画面层(单机)或房间的互动钩子里算。
11
11
  * - kit 不认识任何游戏:短语表、名场面的映射、电脑人物都由游戏给。
12
12
  */
13
+ import { applyTheme, type KitTheme } from './theme.js';
13
14
  import { PROPS, STICKERS, createFunLayer, propSvg, showSticker, stickerSvg, throwAt, type FunLayer, type FunPhase, type PropId, type RectLike, type StickerId } from './fun.js';
14
15
  import {
15
16
  BOT_REACT,
@@ -94,6 +95,8 @@ export interface TableFunOptions {
94
95
  log?(line: string): void;
95
96
  /** 效果图层的 z-index,菜单和热区在它上面一层;默认 50 */
96
97
  zIndex?: number;
98
+ /** 主题(0.16.0):写到菜单根节点(.atf-ui)上的 `--ak-*` 变量;不传就用页面 CSS 里的变量或默认外观 */
99
+ theme?: KitTheme;
97
100
  /** 电脑反应的参数,默认规范的数字 */
98
101
  react?: BotReactConfig;
99
102
  /** 测试、复现用 */
@@ -113,26 +116,27 @@ const DEFAULT_LINES: Record<FunMood, readonly string[]> = {
113
116
  const HINT_TEXT = '点头像扔道具,长按连扔';
114
117
  const CSS_ID = 'arcade-table-fun-css';
115
118
  const CSS = `
116
- .atf-ui{position:fixed;inset:0;pointer-events:none;font-family:inherit;-webkit-tap-highlight-color:transparent}
119
+ .atf-ui{position:fixed;inset:0;pointer-events:none;font-family:var(--ak-font,inherit);-webkit-tap-highlight-color:transparent}
117
120
  .atf-hot{position:absolute;left:0;top:0;pointer-events:auto;border-radius:50%;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;user-select:none}
118
121
  .atf-away{position:absolute;inset:0;pointer-events:auto}
119
- .atf-box{position:absolute;left:0;top:0;pointer-events:auto;background:rgba(43,29,20,.96);border:2px solid #d4a64a;border-radius:14px;padding:8px;box-sizing:border-box;color:#fff6e0;box-shadow:0 6px 18px rgba(0,0,0,.45);transform-origin:center;animation:atf-pop .14s ease-out}
120
- .atf-title{font-size:15px;font-weight:700;color:#f3d98b;margin:0 4px 6px}
122
+ .atf-box{position:absolute;left:0;top:0;pointer-events:auto;background:var(--ak-bg,rgba(43,29,20,.96));border:2px solid var(--ak-border,#d4a64a);border-radius:var(--ak-radius,14px);padding:8px;box-sizing:border-box;color:var(--ak-fg,#fff6e0);box-shadow:0 6px 18px rgba(0,0,0,.45);transform-origin:center;animation:atf-pop .14s ease-out}
123
+ .atf-title{font-size:15px;font-weight:700;color:var(--ak-title,#f3d98b);margin:0 4px 6px}
121
124
  .atf-row{display:flex;gap:4px;flex-wrap:wrap}
122
- .atf-cell{position:relative;width:48px;height:48px;border:0;border-radius:10px;background:#4a3220;padding:4px;box-sizing:border-box;cursor:pointer;overflow:hidden}
125
+ .atf-cell{position:relative;width:48px;height:48px;border:0;border-radius:var(--ak-radius-btn,10px);background:var(--ak-soft,#4a3220);padding:4px;box-sizing:border-box;cursor:pointer;overflow:hidden}
123
126
  .atf-cell svg{width:100%;height:100%;display:block;pointer-events:none}
124
- .atf-chip{min-height:44px;border:1px solid rgba(212,166,74,.6);border-radius:22px;background:#7a2a1e;color:#fff6e0;font:inherit;font-size:14px;font-weight:700;padding:0 12px;cursor:pointer}
125
- .atf-chip.atf-soft{background:#4a3220}.atf-chip.atf-grey{background:#3a3a3a}.atf-chip.atf-on{background:#b3202a}.atf-chip.atf-pick{background:#3a6a2a}
127
+ .atf-chip{min-height:44px;border:1px solid var(--ak-border,rgba(212,166,74,.6));border-radius:var(--ak-radius-btn,22px);background:var(--ak-accent,#7a2a1e);color:var(--ak-accent-fg,#fff6e0);font:inherit;font-size:14px;font-weight:700;padding:0 12px;cursor:pointer}
128
+ .atf-chip.atf-soft,.atf-chip.atf-grey,.atf-chip.atf-on,.atf-chip.atf-pick{color:var(--ak-fg,#fff6e0)}
129
+ .atf-chip.atf-soft{background:var(--ak-soft,#4a3220)}.atf-chip.atf-grey{background:#3a3a3a}.atf-chip.atf-on{background:#b3202a}.atf-chip.atf-pick{background:#3a6a2a}
126
130
  .atf-grid{display:grid;gap:4px}
127
131
  .atf-scroll{overflow-y:auto;overscroll-behavior:contain}
128
- .atf-cool{position:absolute;inset:0;border-radius:10px;background:rgba(0,0,0,.5);pointer-events:none}
129
- .atf-cool::after{content:'';position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:#f3d98b;animation:atf-spin .7s linear infinite}
130
- .atf-btn{position:absolute;left:0;top:0;pointer-events:auto;min-width:44px;min-height:44px;border-radius:22px;border:2px solid #d4a64a;background:#5a3a22;color:#fff6e0;font:inherit;font-size:13px;font-weight:700;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;padding:2px 6px;box-sizing:border-box}
132
+ .atf-cool{position:absolute;inset:0;border-radius:var(--ak-radius-btn,10px);background:rgba(0,0,0,.5);pointer-events:none}
133
+ .atf-cool::after{content:'';position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:var(--ak-title,#f3d98b);animation:atf-spin .7s linear infinite}
134
+ .atf-btn{position:absolute;left:0;top:0;pointer-events:auto;min-width:44px;min-height:44px;border-radius:var(--ak-radius-btn,22px);border:2px solid var(--ak-border,#d4a64a);background:var(--ak-soft,#5a3a22);color:var(--ak-fg,#fff6e0);font:inherit;font-size:13px;font-weight:700;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;padding:2px 6px;box-sizing:border-box}
131
135
  .atf-btn svg{width:22px;height:22px}
132
- .atf-bubble{position:absolute;left:0;top:0;max-width:220px;padding:6px 12px;border-radius:14px;background:#fffaf0;color:#2b1d14;font-size:15px;font-weight:700;box-shadow:0 3px 8px rgba(0,0,0,.35);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
133
- .atf-flash{position:absolute;left:0;top:0;font-size:26px;font-weight:900;color:#f3d98b;-webkit-text-stroke:1.5px #2b1d14;pointer-events:none;white-space:nowrap}
134
- .atf-banner{position:absolute;left:50%;top:18%;padding:10px 34px;background:linear-gradient(#c8323a,#8a1a20);border:2px solid #f3d98b;border-radius:6px;color:#fff6e0;font-size:28px;font-weight:900;letter-spacing:4px;text-align:center;pointer-events:none;box-shadow:0 6px 20px rgba(0,0,0,.4)}
135
- .atf-banner small{display:block;font-size:15px;letter-spacing:1px;color:#f3d98b}
136
+ .atf-bubble{position:absolute;left:0;top:0;max-width:220px;padding:6px 12px;border-radius:var(--ak-radius,14px);background:#fffaf0;color:#2b1d14;font-size:15px;font-weight:700;box-shadow:0 3px 8px rgba(0,0,0,.35);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
137
+ .atf-flash{position:absolute;left:0;top:0;font-size:26px;font-weight:900;color:var(--ak-title,#f3d98b);-webkit-text-stroke:1.5px #2b1d14;pointer-events:none;white-space:nowrap}
138
+ .atf-banner{position:absolute;left:50%;top:18%;padding:10px 34px;background:linear-gradient(#c8323a,#8a1a20);border:2px solid var(--ak-title,#f3d98b);border-radius:6px;color:var(--ak-fg,#fff6e0);font-size:28px;font-weight:900;letter-spacing:4px;text-align:center;pointer-events:none;box-shadow:0 6px 20px rgba(0,0,0,.4)}
139
+ .atf-banner small{display:block;font-size:15px;letter-spacing:1px;color:var(--ak-title,#f3d98b)}
136
140
  @keyframes atf-pop{from{opacity:0;scale:.92}to{opacity:1;scale:1}}
137
141
  @keyframes atf-spin{to{transform:rotate(360deg)}}
138
142
  `;
@@ -193,6 +197,7 @@ export class TableFun {
193
197
  this.ui = this.doc.createElement('div');
194
198
  this.ui.className = 'atf-ui';
195
199
  this.ui.style.zIndex = String(z + 1);
200
+ applyTheme(this.ui, opts.theme);
196
201
  this.doc.body.appendChild(this.ui);
197
202
  window.addEventListener('resize', this.onResize);
198
203
  this.refresh();
package/src/theme.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * DOM 组件的主题(0.16.0):颜色、圆角、字体走 CSS 自定义属性 `--ak-*`,每处写成 `var(--ak-xxx, <原来的值>)`,
3
+ * 不给主题时外观和以前完全一样。两种用法:组件的选项里传 `theme`(写到组件根节点上),或游戏自己的 CSS 在 :root / body 上写 `--ak-*`(靠继承)。
4
+ * 纯函数,import 时不碰 DOM。值来自游戏代码,仍要挡 CSS 注入:含 `;` `{` `}` `<` `url(` `expression` 等的字段整个忽略。
5
+ */
6
+
7
+ /** 主题字段,全是可选的字符串(合法的 CSS 值,如 `#2b1d14`、`14px`、`"Noto Serif SC",serif`) */
8
+ export interface KitTheme {
9
+ /** --ak-bg:面板、菜单、横条的底色 */
10
+ bg?: string;
11
+ /** --ak-fg:面板上的文字色(分享预览的字和次按钮的边、字也用它) */
12
+ fg?: string;
13
+ /** --ak-accent:主按钮底色(去、叫他来、互动里的主操作、保存/复制图片) */
14
+ accent?: string;
15
+ /** --ak-accent-fg:主按钮上的文字色 */
16
+ accentFg?: string;
17
+ /** --ak-border:面板、按钮的边框色 */
18
+ border?: string;
19
+ /** --ak-radius:面板、横条、气泡、预览图的圆角 */
20
+ radius?: string;
21
+ /** --ak-radius-btn:按钮、格子、好友行的圆角 */
22
+ radiusBtn?: string;
23
+ /** --ak-font:字体(font-family 的值) */
24
+ font?: string;
25
+ /** --ak-overlay:遮罩色(分享预览、叫朋友面板背后那层) */
26
+ overlay?: string;
27
+ /** --ak-soft:次按钮、格子、好友行的底色 */
28
+ soft?: string;
29
+ /** --ak-title:标题、✕、强调字(如飘字、横幅的金色)的颜色 */
30
+ title?: string;
31
+ }
32
+
33
+ const NAMES: ReadonlyArray<readonly [keyof KitTheme, string]> = [
34
+ ['bg', '--ak-bg'],
35
+ ['fg', '--ak-fg'],
36
+ ['accent', '--ak-accent'],
37
+ ['accentFg', '--ak-accent-fg'],
38
+ ['border', '--ak-border'],
39
+ ['radius', '--ak-radius'],
40
+ ['radiusBtn', '--ak-radius-btn'],
41
+ ['font', '--ak-font'],
42
+ ['overlay', '--ak-overlay'],
43
+ ['soft', '--ak-soft'],
44
+ ['title', '--ak-title'],
45
+ ];
46
+
47
+ /** 能跳出一条声明、一个块,或带出外部资源、脚本的写法 */
48
+ const UNSAFE = /[;{}<>\\]|\/\*|url\s*\(|expression|@import|javascript:/i;
49
+
50
+ /** 值是不是能放进 CSS 自定义属性:非空字符串、不太长、不含注入写法 */
51
+ export function safeThemeValue(v: unknown): v is string {
52
+ return typeof v === 'string' && v.trim() !== '' && v.length <= 200 && !UNSAFE.test(v);
53
+ }
54
+
55
+ /** 主题对象 → [变量名, 值] 列表;不认识的字段、不合格的值忽略,空对象(或不是对象)给空列表 */
56
+ export function themeVars(theme: KitTheme | null | undefined): Array<[string, string]> {
57
+ const out: Array<[string, string]> = [];
58
+ if (!theme || typeof theme !== 'object') return out;
59
+ for (const [key, name] of NAMES) {
60
+ const v = theme[key];
61
+ if (safeThemeValue(v)) out.push([name, v.trim()]);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** 把主题写到组件根节点上;没传、没有合格字段就什么也不写(游戏可以只在自己的 CSS 里写 `--ak-*`) */
67
+ export function applyTheme(el: HTMLElement, theme: KitTheme | null | undefined): void {
68
+ for (const [name, v] of themeVars(theme)) el.style.setProperty(name, v);
69
+ }