openclacky 1.2.15 → 1.2.17

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84e7378b08b627bad34d327d1bd82cc7efbfe980a690d64e678242917be8125d
4
- data.tar.gz: 87e4c1b8e99f2195c98c85124816503fd11436b3bdb38465bb7289fab1204fa3
3
+ metadata.gz: 3a0f79d1b7995b24b2b1a5d31dc36d4342ad1ff925b66749b7edb6268d0bcf29
4
+ data.tar.gz: b1f583cde8ffb619a4cb3558f074c369251c5445acce05116d1bc415e94eb3b4
5
5
  SHA512:
6
- metadata.gz: 36cb343f4a81222b3a2861dcd80529f0d3216a341e19ea7ebfd1ea6dbebded9c0d31a212a645cffb7268e9faf844be9f257d739677a17d0387bf033970dc7675
7
- data.tar.gz: 16d08fc33223c56024a27072de5a0f435ac969c1ac55fffa39738e0e8d9bfe77f1ebbe4c4cafb8793c2dac1367744daeb417b97db9a854ba7e04ffd4d2b17042
6
+ metadata.gz: 18921df7e5d4d4a7f3cbf1e19bd1b65bbc8360fa064d81ffc129375d86ec9a07ebfc324414d67a7d6df9782dfd8f966065755d0224d8af77b92604d6320195e2
7
+ data.tar.gz: 013db4dddea3c901bd3e8885241676b7fdd2ca2856a9a4df4cfde4b2475b7e57dc7795386d53db934acfe5786515501fd724e3ef559e8d5e786a6b07a3312db6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.17] - 2026-06-12
9
+
10
+ ### Added
11
+ - Session sharing to Web UI — share any session via a shareable link with billing integration
12
+ - Share telemetry tracking
13
+
14
+ ### Fixed
15
+ - Markdown rendering in certain edge cases
16
+ - Image blocks not detected in replay round counting, potentially causing history truncation
17
+ - History images served as base64 causing replay lag, now proxied through server
18
+ - WSL kernel repair getting stuck in infinite loop on pending state
19
+ - WeChat QR login fallback showing false stale-session errors
20
+
21
+ ### More
22
+ - Background color styling update
23
+
24
+ ## [1.2.16] - 2026-06-10
25
+
26
+ ### Added
27
+ - Claude Fable 5 model support
28
+
29
+ ### Fixed
30
+ - Model test failed when using saved API key
31
+ - Windows WSL install success rate
32
+
8
33
  ## [1.2.15] - 2026-06-10
9
34
 
10
35
  ### Added
@@ -201,8 +201,11 @@ module Clacky
201
201
  if msg[:content].is_a?(String)
202
202
  !msg[:content].start_with?("[SYSTEM]")
203
203
  elsif msg[:content].is_a?(Array)
204
- # Must contain at least one text or image block (not a tool_result array)
205
- msg[:content].any? { |b| b.is_a?(Hash) && %w[text image].include?(b[:type].to_s) }
204
+ # Must contain at least one text or image block (not a tool_result array).
205
+ # "image_url" covers image-only messages (user sent a picture with no
206
+ # accompanying text); without it such messages start no round and get
207
+ # dropped on replay, making the image vanish on session reopen.
208
+ msg[:content].any? { |b| b.is_a?(Hash) && %w[text image image_url].include?(b[:type].to_s) }
206
209
  else
207
210
  false
208
211
  end
@@ -253,13 +253,15 @@ browser(action="navigate", url="<qr_page_url>")
253
253
  >
254
254
  > `http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/weixin-qr.html?url=<URL-encoded qrcode_url>`
255
255
  >
256
- > Scan the QR code with WeChat, confirm in the app, then reply "done".
256
+ > Scan the QR code with WeChat and confirm in the app. I'm already watching for your scan — no need to reply.
257
+
258
+ **Do NOT wait for the user to reply "done".** Immediately proceed to Step 3 and start polling — exactly as in the browser-succeeds path. The polling script must already be running while the user scans, so it can observe the `scaned → confirmed` transition; otherwise a real scan can be misread as a stale session.
257
259
 
258
260
  The page renders a proper scannable QR code image. Do NOT open the raw `qrcode_url` directly — that page shows "请使用微信扫码打开" with no actual QR image.
259
261
 
260
262
  #### Step 3 — Wait for scan and save credentials
261
263
 
262
- Once the browser shows the QR page, immediately run the polling script in the background:
264
+ As soon as the QR page has been presented to the user — whether you opened it via the browser tool **or** gave the user the manual link — immediately run the polling script in the background. **In both cases, do NOT wait for the user to confirm or reply "done" before starting the poll** — the script must already be running while the user scans:
263
265
 
264
266
  ```bash
265
267
  ruby "SKILL_DIR/weixin_setup.rb" --qrcode-id "$QRCODE_ID"
@@ -31,6 +31,7 @@ module Clacky
31
31
  "api" => "bedrock",
32
32
  "default_model" => "abs-claude-sonnet-4-6",
33
33
  "models" => [
34
+ "abs-claude-fable-5",
34
35
  "abs-claude-opus-4-8",
35
36
  "abs-claude-opus-4-7",
36
37
  "abs-claude-opus-4-6",
@@ -80,6 +81,7 @@ module Clacky
80
81
  # themselves, so they're intentionally not listed here as keys —
81
82
  # no injection happens when the default model is already lite-class.
82
83
  "lite_models" => {
84
+ "abs-claude-fable-5" => "abs-claude-haiku-4-5",
83
85
  "abs-claude-opus-4-8" => "abs-claude-haiku-4-5",
84
86
  "abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
85
87
  "abs-claude-opus-4-6" => "abs-claude-haiku-4-5",
@@ -91,6 +93,7 @@ module Clacky
91
93
  # Fallback chain: if a model is unavailable, try the next one in order.
92
94
  # Keys are primary model names; values are the fallback model to use instead.
93
95
  "fallback_models" => {
96
+ "abs-claude-fable-5" => "abs-claude-opus-4-8",
94
97
  "abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5"
95
98
  },
96
99
  "website_url" => "https://www.openclacky.com/ai-keys"
@@ -45,7 +45,12 @@ module Clacky
45
45
  if url
46
46
  url
47
47
  elsif type.to_s == "image" && path && File.exist?(path.to_s)
48
- Utils::FileProcessor.image_path_to_data_url(path) rescue "expired:#{name}"
48
+ # Serve via the /api/local-image proxy instead of inlining a base64
49
+ # data URL. Inlining forced a synchronous disk-read + full base64
50
+ # encode + downscale on every history replay (2-3s lag for sessions
51
+ # with downgraded text-model images). The proxy lets the browser
52
+ # lazy-load + cache the image, keeping the replay response tiny.
53
+ "/api/local-image?path=#{CGI.escape(path.to_s)}"
49
54
  elsif name
50
55
  type.to_s == "image" ? "expired:#{name}" : "pdf:#{name}"
51
56
  end
@@ -448,6 +453,7 @@ module Clacky
448
453
  when ["POST", "/api/browser/configure"] then api_browser_configure(req, res)
449
454
  when ["POST", "/api/browser/reload"] then api_browser_reload(res)
450
455
  when ["POST", "/api/browser/toggle"] then api_browser_toggle(res)
456
+ when ["POST", "/api/telemetry"] then api_telemetry(req, res)
451
457
  when ["POST", "/api/onboard/complete"] then api_onboard_complete(req, res)
452
458
  when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
453
459
  when ["GET", "/api/store/skills"] then api_store_skills(res)
@@ -854,6 +860,17 @@ module Clacky
854
860
  json_response(res, 500, { ok: false, error: e.message })
855
861
  end
856
862
 
863
+ # POST /api/telemetry
864
+ # Body: { "event": "share_open" | "share_download", ... }
865
+ # Fire-and-forget telemetry from the WebUI frontend.
866
+ def api_telemetry(req, res)
867
+ body = parse_json_body(req) || {}
868
+ Clacky::Telemetry.share!(event: body["event"], extra: body["extra"])
869
+ json_response(res, 200, { ok: true })
870
+ rescue StandardError => e
871
+ json_response(res, 500, { ok: false, error: e.message })
872
+ end
873
+
857
874
  # POST /api/media/image
858
875
  # Body: { "prompt": "...", "aspect_ratio": "landscape|square|portrait",
859
876
  # "output_dir": "<absolute path, optional>" }
@@ -1442,6 +1459,8 @@ module Clacky
1442
1459
  branded: true,
1443
1460
  needs_activation: true,
1444
1461
  product_name: brand.product_name,
1462
+ homepage_url: brand.homepage_url,
1463
+ logo_url: brand.logo_url,
1445
1464
  test_mode: @brand_test,
1446
1465
  distribution_refresh_pending: refresh_pending
1447
1466
  })
@@ -1481,6 +1500,8 @@ module Clacky
1481
1500
  branded: true,
1482
1501
  needs_activation: false,
1483
1502
  product_name: brand.product_name,
1503
+ homepage_url: brand.homepage_url,
1504
+ logo_url: brand.logo_url,
1484
1505
  warning: warning,
1485
1506
  test_mode: @brand_test,
1486
1507
  user_licensed: brand.user_licensed?,
@@ -4141,8 +4162,16 @@ module Clacky
4141
4162
 
4142
4163
  api_key = body["api_key"].to_s
4143
4164
  if api_key.include?("****")
4144
- idx = body["index"]&.to_i || @agent_config.current_model_index
4145
- api_key = @agent_config.models.dig(idx, "api_key").to_s
4165
+ model_id = body["id"].to_s
4166
+ entry = nil
4167
+ if !model_id.empty?
4168
+ entry = @agent_config.models.find { |m| m["id"] == model_id }
4169
+ end
4170
+ if entry.nil? && body.key?("index")
4171
+ entry = @agent_config.models[body["index"].to_i]
4172
+ end
4173
+ entry ||= @agent_config.models[@agent_config.current_model_index]
4174
+ api_key = entry ? entry["api_key"].to_s : ""
4146
4175
  end
4147
4176
 
4148
4177
  model = body["model"].to_s
@@ -71,6 +71,26 @@ module Clacky
71
71
  fire_and_forget("/api/v1/telemetry/task", payload.compact)
72
72
  end
73
73
 
74
+ # Called from the WebUI when user opens the share modal or completes
75
+ # a share action (e.g. downloads poster). Tracks share engagement.
76
+ #
77
+ # @param event [String] "share_open" or "share_download"
78
+ # @param extra [Hash] optional context (platform, scorecard mode, etc.)
79
+ def share!(event:, extra: {})
80
+ return unless enabled?
81
+
82
+ brand = Clacky::BrandConfig.load
83
+ payload = {
84
+ device_id: resolve_device_id(brand),
85
+ version: Clacky::VERSION,
86
+ brand: brand.branded? ? brand.package_name : nil,
87
+ event: event
88
+ }
89
+ payload.merge!(extra) if extra.is_a?(Hash)
90
+
91
+ fire_and_forget("/api/v1/telemetry/share", payload.compact)
92
+ end
93
+
74
94
  # ── private helpers ────────────────────────────────────────────────
75
95
 
76
96
  private def enabled?
@@ -8,6 +8,21 @@ module Clacky
8
8
  # All pricing is based on official API documentation
9
9
  PRICING_TABLE = {
10
10
  # Claude 4.5 models - tiered pricing based on prompt length
11
+ "claude-fable-5" => {
12
+ input: {
13
+ default: 10.00, # $10/MTok for prompts ≤ 200K tokens
14
+ over_200k: 10.00 # same for all tiers
15
+ },
16
+ output: {
17
+ default: 50.00, # $50/MTok for prompts ≤ 200K tokens
18
+ over_200k: 50.00 # same for all tiers
19
+ },
20
+ cache: {
21
+ write: 12.50, # $12.50/MTok cache write (5-min tier)
22
+ read: 1.00 # $1.00/MTok cache read
23
+ }
24
+ },
25
+
11
26
  "claude-opus-4.5" => {
12
27
  input: {
13
28
  default: 5.00, # $5/MTok for prompts ≤ 200K tokens
@@ -633,6 +648,8 @@ module Clacky
633
648
  # Support both dot and dash separators (e.g., "4.5", "4-5", "4-6")
634
649
  # Also handles Bedrock cross-region prefixes (e.g. "jp.anthropic.claude-sonnet-4-6")
635
650
  case model
651
+ when /claude.*fable.*5/i
652
+ "claude-fable-5"
636
653
  when /claude.*opus.*4[.-]?[5-9]/i
637
654
  "claude-opus-4.5"
638
655
  when /claude.*sonnet.*4[.-]?[5-9]/i
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.2.15"
4
+ VERSION = "1.2.17"
5
5
  end
@@ -9150,6 +9150,22 @@ body.setup-mode[data-theme="dark"] {
9150
9150
  color: var(--color-text-primary);
9151
9151
  border-color: var(--color-error);
9152
9152
  }
9153
+ .billing-share-btn {
9154
+ padding: 0.5rem 0.75rem;
9155
+ border: 1px solid var(--color-accent, #38bdf8);
9156
+ border-radius: 8px;
9157
+ background: var(--color-bg-secondary);
9158
+ color: var(--color-accent, #38bdf8);
9159
+ font-size: 0.8125rem;
9160
+ font-weight: 600;
9161
+ white-space: nowrap;
9162
+ cursor: pointer;
9163
+ transition: all 0.2s ease;
9164
+ }
9165
+ .billing-share-btn:hover {
9166
+ background: var(--color-accent, #38bdf8);
9167
+ color: #fff;
9168
+ }
9153
9169
  .billing-clear-popup {
9154
9170
  position: absolute;
9155
9171
  top: 100%;
@@ -9238,6 +9254,51 @@ body.setup-mode[data-theme="dark"] {
9238
9254
  }
9239
9255
 
9240
9256
  /* ── Chart Row (Single Line) ─────────────────────────────────────────── */
9257
+ .billing-heatmap-row { width: 100%; }
9258
+ .billing-heat-dow-row {
9259
+ display: grid;
9260
+ grid-template-columns: repeat(7, 32px);
9261
+ gap: 6px;
9262
+ margin: 0.75rem 0 6px;
9263
+ justify-content: start;
9264
+ }
9265
+ .billing-heat-dow {
9266
+ text-align: center;
9267
+ font-size: 11px;
9268
+ color: var(--color-text-tertiary, var(--color-text-secondary));
9269
+ }
9270
+ .billing-heat-grid {
9271
+ display: grid;
9272
+ grid-template-columns: repeat(7, 32px);
9273
+ gap: 6px;
9274
+ justify-content: start;
9275
+ }
9276
+ .billing-heat-cell {
9277
+ width: 32px;
9278
+ height: 32px;
9279
+ border-radius: 6px;
9280
+ background: var(--color-bg-primary);
9281
+ }
9282
+ .billing-heat-cell.is-empty { background: transparent; }
9283
+ .billing-heat-cell[data-level="0"] { background: var(--color-bg-primary); border: 1px solid var(--color-border-primary); }
9284
+ .billing-heat-cell[data-level="1"] { background: #9be9a8; }
9285
+ .billing-heat-cell[data-level="2"] { background: #40c463; }
9286
+ .billing-heat-cell[data-level="3"] { background: #30a14e; }
9287
+ .billing-heat-cell[data-level="4"] { background: #216e39; }
9288
+ .billing-heat-cell[data-level="5"] { background: #0a4020; }
9289
+ .billing-heat-legend {
9290
+ display: flex;
9291
+ align-items: center;
9292
+ gap: 5px;
9293
+ font-size: 11px;
9294
+ color: var(--color-text-secondary);
9295
+ }
9296
+ .billing-heat-legend .billing-heat-cell {
9297
+ width: 13px;
9298
+ height: 13px;
9299
+ aspect-ratio: auto;
9300
+ }
9301
+
9241
9302
  .billing-chart-row {
9242
9303
  width: 100%;
9243
9304
  }
@@ -10363,3 +10424,247 @@ body.setup-mode[data-theme="dark"] {
10363
10424
  .msg-phase-empty .msg-phase-summary { cursor: default; }
10364
10425
  .msg-phase-empty .msg-phase-summary::before { visibility: hidden; }
10365
10426
  .msg-phase-empty .msg-phase-body { display: none; }
10427
+
10428
+ /* ── Share modal ──────────────────────────────────────────────────────── */
10429
+ .share-overlay {
10430
+ position: fixed;
10431
+ inset: 0;
10432
+ z-index: 1200;
10433
+ display: flex;
10434
+ align-items: center;
10435
+ justify-content: center;
10436
+ background: var(--color-bg-overlay);
10437
+ opacity: 0;
10438
+ transition: opacity .18s ease;
10439
+ padding: 20px;
10440
+ }
10441
+ .share-overlay.open { opacity: 1; }
10442
+
10443
+ .share-modal {
10444
+ position: relative;
10445
+ width: 100%;
10446
+ max-width: 660px;
10447
+ max-height: 90vh;
10448
+ overflow-y: auto;
10449
+ background: var(--color-bg-card);
10450
+ border: 1px solid var(--color-border-primary);
10451
+ border-radius: var(--radius-lg, 14px);
10452
+ box-shadow: 0 18px 48px rgba(15, 18, 28, 0.22);
10453
+ padding: 24px;
10454
+ transform: translateY(8px) scale(.98);
10455
+ transition: transform .18s ease;
10456
+ }
10457
+ .share-overlay.open .share-modal { transform: translateY(0) scale(1); }
10458
+
10459
+ .share-close {
10460
+ position: absolute;
10461
+ top: 12px;
10462
+ right: 14px;
10463
+ border: none;
10464
+ background: none;
10465
+ font-size: 22px;
10466
+ line-height: 1;
10467
+ color: var(--color-text-muted);
10468
+ cursor: pointer;
10469
+ }
10470
+ .share-close:hover { color: var(--color-text-primary); }
10471
+
10472
+ .share-title {
10473
+ margin: 0 0 4px;
10474
+ font-size: 17px;
10475
+ font-weight: 700;
10476
+ color: var(--color-text-primary);
10477
+ }
10478
+ .share-subtitle {
10479
+ margin: 0 0 16px;
10480
+ font-size: 13px;
10481
+ color: var(--color-text-tertiary);
10482
+ }
10483
+
10484
+ .share-body {
10485
+ display: flex;
10486
+ gap: 20px;
10487
+ align-items: flex-start;
10488
+ }
10489
+ .share-controls {
10490
+ flex: 1;
10491
+ min-width: 0;
10492
+ }
10493
+
10494
+ .share-poster-wrap {
10495
+ display: flex;
10496
+ justify-content: center;
10497
+ flex-shrink: 0;
10498
+ }
10499
+ .share-poster-img {
10500
+ width: 220px;
10501
+ border-radius: var(--radius-md, 10px);
10502
+ box-shadow: 0 6px 18px rgba(15, 18, 28, 0.16);
10503
+ }
10504
+
10505
+ @media (max-width: 560px) {
10506
+ .share-body { flex-direction: column; align-items: stretch; }
10507
+ .share-poster-wrap { margin-bottom: 4px; }
10508
+ .share-poster-img { width: 180px; }
10509
+ }
10510
+
10511
+ .share-row-label {
10512
+ font-size: 12px;
10513
+ font-weight: 600;
10514
+ color: var(--color-text-tertiary);
10515
+ }
10516
+
10517
+ .share-theme-row {
10518
+ display: flex;
10519
+ flex-direction: column;
10520
+ gap: 6px;
10521
+ margin-bottom: 14px;
10522
+ }
10523
+ .share-theme-chips {
10524
+ display: flex;
10525
+ gap: 8px;
10526
+ }
10527
+ .share-theme-chip {
10528
+ flex: 1;
10529
+ height: 34px;
10530
+ border: 2px solid transparent;
10531
+ border-radius: var(--radius-sm, 6px);
10532
+ cursor: pointer;
10533
+ position: relative;
10534
+ display: flex;
10535
+ align-items: center;
10536
+ justify-content: center;
10537
+ transition: border-color .12s ease, transform .12s ease;
10538
+ }
10539
+ .share-theme-chip:hover { transform: translateY(-1px); }
10540
+ .share-theme-chip.is-active { border-color: var(--color-button-primary); }
10541
+ .share-theme-name {
10542
+ font-size: 11px;
10543
+ font-weight: 700;
10544
+ color: rgba(0,0,0,.55);
10545
+ text-shadow: 0 1px 0 rgba(255,255,255,.4);
10546
+ pointer-events: none;
10547
+ }
10548
+ .share-theme-chip[data-theme="geek"] .share-theme-name {
10549
+ color: rgba(255,255,255,.92);
10550
+ text-shadow: 0 1px 2px rgba(0,0,0,.4);
10551
+ }
10552
+
10553
+ .share-periods {
10554
+ display: inline-flex;
10555
+ gap: 4px;
10556
+ padding: 3px;
10557
+ margin-bottom: 12px;
10558
+ border: 1px solid var(--color-border-primary);
10559
+ border-radius: var(--radius-sm, 6px);
10560
+ background: var(--color-bg-primary);
10561
+ }
10562
+ .share-period {
10563
+ padding: 6px 16px;
10564
+ border: none;
10565
+ border-radius: var(--radius-sm, 5px);
10566
+ background: transparent;
10567
+ color: var(--color-text-secondary);
10568
+ font-size: 13px;
10569
+ font-weight: 600;
10570
+ cursor: pointer;
10571
+ transition: background .12s ease, color .12s ease;
10572
+ }
10573
+ .share-period:hover { color: var(--color-text-primary); }
10574
+ .share-period.is-active {
10575
+ background: var(--color-button-primary);
10576
+ color: var(--color-button-primary-text);
10577
+ }
10578
+
10579
+ .share-platforms {
10580
+ display: grid;
10581
+ grid-template-columns: repeat(4, 1fr);
10582
+ gap: 8px;
10583
+ margin-bottom: 14px;
10584
+ }
10585
+ .share-platform {
10586
+ padding: 9px 4px;
10587
+ border: 1px solid var(--color-border-primary);
10588
+ border-radius: var(--radius-sm, 6px);
10589
+ background: var(--color-bg-primary);
10590
+ color: var(--color-text-secondary);
10591
+ font-size: 13px;
10592
+ font-weight: 600;
10593
+ cursor: pointer;
10594
+ transition: background .12s ease, border-color .12s ease, color .12s ease;
10595
+ }
10596
+ .share-platform:hover {
10597
+ background: var(--color-bg-hover);
10598
+ border-color: var(--color-border-strong);
10599
+ }
10600
+ .share-platform.is-active {
10601
+ background: var(--color-button-primary);
10602
+ color: var(--color-button-primary-text);
10603
+ border-color: var(--color-button-primary);
10604
+ }
10605
+
10606
+ .share-editor { margin-bottom: 14px; }
10607
+ .share-editor-head {
10608
+ display: flex;
10609
+ align-items: center;
10610
+ justify-content: space-between;
10611
+ margin-bottom: 6px;
10612
+ }
10613
+ .share-shuffle {
10614
+ border: none;
10615
+ background: none;
10616
+ color: var(--color-button-primary);
10617
+ font-size: 12px;
10618
+ font-weight: 600;
10619
+ cursor: pointer;
10620
+ padding: 2px 4px;
10621
+ }
10622
+ .share-shuffle:hover { text-decoration: underline; }
10623
+ .share-text {
10624
+ width: 100%;
10625
+ box-sizing: border-box;
10626
+ resize: vertical;
10627
+ min-height: 72px;
10628
+ padding: 10px 12px;
10629
+ border: 1px solid var(--color-border-primary);
10630
+ border-radius: var(--radius-sm, 6px);
10631
+ background: var(--color-bg-primary);
10632
+ color: var(--color-text-primary);
10633
+ font-size: 13px;
10634
+ line-height: 1.5;
10635
+ font-family: inherit;
10636
+ }
10637
+ .share-text:focus {
10638
+ outline: none;
10639
+ border-color: var(--color-button-primary);
10640
+ }
10641
+
10642
+ .share-actions {
10643
+ display: flex;
10644
+ flex-wrap: wrap;
10645
+ gap: 8px;
10646
+ }
10647
+ .share-btn-primary, .share-btn-secondary {
10648
+ padding: 10px;
10649
+ border-radius: var(--radius-sm, 6px);
10650
+ font-size: 13px;
10651
+ font-weight: 600;
10652
+ cursor: pointer;
10653
+ border: 1px solid var(--color-border-primary);
10654
+ }
10655
+ .share-btn-primary {
10656
+ flex: 1 1 100%;
10657
+ background: var(--color-button-primary);
10658
+ color: var(--color-button-primary-text);
10659
+ border-color: var(--color-button-primary);
10660
+ }
10661
+ .share-btn-primary:hover { background: var(--color-button-primary-hover); }
10662
+ .share-btn-secondary {
10663
+ flex: 1 1 calc(50% - 4px);
10664
+ min-width: 90px;
10665
+ background: var(--color-bg-primary);
10666
+ color: var(--color-text-secondary);
10667
+ }
10668
+ .share-btn-secondary:hover { background: var(--color-bg-hover); }
10669
+
10670
+ #share-toggle-header svg { display: block; }