openclacky 1.2.16 → 1.2.18

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.
@@ -442,8 +442,10 @@ module Clacky
442
442
  Commands:
443
443
  ? / h / help - show this help
444
444
  /new / /clear - start a new session
445
- /model - show current model & available models
446
- /model <n> - switch to model n
445
+ /model - show current model, cards & quick-switch list
446
+ /model <n> - switch card by number
447
+ /model s<n> - quick-switch model under current card
448
+ /model off - reset to card default
447
449
  /skills - list available skills
448
450
  /<skill> <args> - invoke a skill directly
449
451
  /bind <n|session_id> - switch to a session (use /list to see numbers)
@@ -472,46 +474,128 @@ module Clacky
472
474
  arg = text.sub(/\A\/model\s*/i, "").strip
473
475
 
474
476
  if arg.empty?
475
- # Show current model and available list
476
- info = agent.current_model_info
477
- current = info&.dig(:model) || "unknown"
478
- sub = info&.dig(:sub_model)
479
- card = info&.dig(:card_model)
480
- header = "Current model: #{current}"
481
- header += " (#{card} · #{sub})" if card && sub && sub != current
482
- header += " (#{card})" if card && !sub
483
-
484
- models = agent.available_models
485
- if models.empty?
486
- adapter.send_text(chat_id, "#{header}\nNo other models available.")
487
- return
488
- end
477
+ show_model_list(adapter, chat_id, agent)
478
+ elsif arg =~ /\A\d+\z/
479
+ switch_model_by_index(adapter, chat_id, agent, arg.to_i - 1)
480
+ elsif arg =~ /\As(\d+)\z/i
481
+ switch_quick_by_index(adapter, chat_id, agent, $1.to_i - 1)
482
+ else
483
+ switch_model_by_name(adapter, chat_id, agent, arg)
484
+ end
485
+ end
489
486
 
487
+ def show_model_list(adapter, chat_id, agent)
488
+ info = agent.current_model_info
489
+ current = info&.dig(:model) || "unknown"
490
+ sub = info&.dig(:sub_model)
491
+ card = info&.dig(:card_model)
492
+
493
+ header = "Current: #{current}"
494
+ header += " (#{card})" if card && sub && sub != current
495
+ header += " (#{card})" if card && !sub
496
+
497
+ result = header
498
+
499
+ # Card list
500
+ models = agent.available_models
501
+ unless models.empty?
490
502
  lines = models.each_with_index.map do |name, i|
491
503
  marker = name == current ? " *" : ""
492
504
  "#{i + 1}. #{name}#{marker}"
493
505
  end
494
- adapter.send_text(chat_id, "#{header}\n\nSwitch with /model <n>:\n#{lines.join("\n")}")
495
- elsif arg =~ /\A\d+\z/
496
- idx = arg.to_i - 1
497
- models = agent.config.models
498
- if idx < 0 || idx >= models.length
499
- adapter.send_text(chat_id, "Invalid model number. Use /model to see available models.")
500
- return
506
+ result += "\n\nCards (/model <n>):\n#{lines.join("\n")}"
507
+ end
508
+
509
+ # Quick-switch models under current provider
510
+ info = agent.current_model_info
511
+ provider_id = Clacky::Providers.find_by_base_url(info&.dig(:base_url))
512
+ if provider_id
513
+ quick = Clacky::Providers.models(provider_id)
514
+ unless quick.empty?
515
+ current_for_quick = sub || current
516
+ quick_lines = quick.each_with_index.map do |name, i|
517
+ marker = name == current_for_quick ? " *" : ""
518
+ " s#{i + 1}. #{name}#{marker}"
519
+ end
520
+ result += "\n\nQuick switch (/model s<n>):\n#{quick_lines.join("\n")}"
521
+ unless quick.include?(current_for_quick)
522
+ result += "\n(#{current_for_quick} not in this provider; switch card first)"
523
+ end
501
524
  end
525
+ end
502
526
 
503
- model_id = models[idx]["id"]
504
- if agent.switch_model_by_id(model_id)
505
- new_info = agent.current_model_info
506
- adapter.send_text(chat_id, "Switched to #{new_info&.dig(:model) || model_id}.")
507
- else
508
- adapter.send_text(chat_id, "Failed to switch model.")
509
- end
527
+ adapter.send_text(chat_id, result)
528
+ end
529
+
530
+ def switch_model_by_index(adapter, chat_id, agent, idx)
531
+ models = agent.config.models
532
+ if idx < 0 || idx >= models.length
533
+ adapter.send_text(chat_id, "Invalid number. Use /model to see available cards.")
534
+ return
535
+ end
536
+
537
+ model_id = models[idx]["id"]
538
+ if agent.switch_model_by_id(model_id)
539
+ new_info = agent.current_model_info
540
+ adapter.send_text(chat_id, "Switched to #{new_info&.dig(:model) || model_id}.")
510
541
  else
511
- adapter.send_text(chat_id, "Usage: /model to list, /model <n> to switch.")
542
+ adapter.send_text(chat_id, "Failed to switch model.")
512
543
  end
513
544
  end
514
545
 
546
+ def switch_quick_by_index(adapter, chat_id, agent, idx)
547
+ info = agent.current_model_info
548
+ provider_id = Clacky::Providers.find_by_base_url(info&.dig(:base_url))
549
+
550
+ unless provider_id
551
+ adapter.send_text(chat_id, "No quick-switch models. Use /model <n> to switch card.")
552
+ return
553
+ end
554
+
555
+ quick = Clacky::Providers.models(provider_id)
556
+ if idx < 0 || idx >= quick.length
557
+ adapter.send_text(chat_id, "Invalid s#{idx + 1}. Use /model to see quick-switch list.")
558
+ return
559
+ end
560
+
561
+ agent.set_session_sub_model(quick[idx])
562
+ new_info = agent.current_model_info
563
+ adapter.send_text(chat_id, "Switched to #{new_info&.dig(:sub_model) || new_info&.dig(:model)}.")
564
+ end
565
+
566
+ def switch_model_by_name(adapter, chat_id, agent, name)
567
+ info = agent.current_model_info
568
+ provider_id = Clacky::Providers.find_by_base_url(info&.dig(:base_url))
569
+
570
+ unless provider_id
571
+ adapter.send_text(chat_id, "Current card has no quick-switch models. Use /model <n> to switch card.")
572
+ return
573
+ end
574
+
575
+ allowed = Clacky::Providers.models(provider_id)
576
+ if allowed.empty?
577
+ adapter.send_text(chat_id, "No quick-switch models available. Use /model <n> to switch card.")
578
+ return
579
+ end
580
+
581
+ # Clear override
582
+ if name =~ /\A(off|clear|none)\z/i
583
+ agent.set_session_sub_model(nil)
584
+ new_info = agent.current_model_info
585
+ adapter.send_text(chat_id, "Back to card default (#{new_info&.dig(:model)}).")
586
+ return
587
+ end
588
+
589
+ unless allowed.include?(name)
590
+ adapter.send_text(chat_id, "'#{name}' not available. Use /model to see quick-switch list.")
591
+ return
592
+ end
593
+
594
+ agent.set_session_sub_model(name)
595
+ new_info = agent.current_model_info
596
+ adapter.send_text(chat_id, "Switched to #{new_info&.dig(:sub_model) || new_info&.dig(:model)}.")
597
+ end
598
+
515
599
  def handle_skills_command(adapter, event)
516
600
  chat_id = event[:chat_id]
517
601
  session_id = resolve_session(event)
@@ -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?,
@@ -3011,6 +3032,7 @@ module Clacky
3011
3032
  description: skill.context_description,
3012
3033
  description_zh: skill.description_zh,
3013
3034
  source: source,
3035
+ always_show: skill.always_show,
3014
3036
  enabled: !skill.disabled?,
3015
3037
  invalid: skill.invalid?,
3016
3038
  warnings: skill.warnings,
@@ -3063,7 +3085,8 @@ module Clacky
3063
3085
  description: skill.description || skill.context_description,
3064
3086
  description_zh: skill.description_zh,
3065
3087
  encrypted: skill.encrypted?,
3066
- source_type: source_type
3088
+ source_type: source_type,
3089
+ always_show: skill.always_show
3067
3090
  }
3068
3091
  end
3069
3092
 
data/lib/clacky/skill.rb CHANGED
@@ -26,7 +26,7 @@ module Clacky
26
26
  model
27
27
  forbidden_tools
28
28
  auto_summarize
29
-
29
+ always-show
30
30
  ].freeze
31
31
 
32
32
  attr_reader :directory, :frontmatter, :source_path
@@ -35,6 +35,7 @@ module Clacky
35
35
  attr_reader :allowed_tools, :context, :agent_type, :argument_hint, :hooks
36
36
  attr_reader :fork_agent, :model, :forbidden_tools, :auto_summarize
37
37
  attr_reader :brand_skill, :brand_config
38
+ attr_reader :always_show
38
39
 
39
40
  # Source location of this skill — set by SkillLoader after registration.
40
41
  # One of: :default, :global_claude, :global_clacky, :project_claude, :project_clacky, :brand
@@ -515,6 +516,7 @@ module Clacky
515
516
  @model = @frontmatter["model"]
516
517
  @forbidden_tools = @frontmatter["forbidden_tools"]
517
518
  @auto_summarize = @frontmatter["auto_summarize"]
519
+ @always_show = @frontmatter["always-show"]
518
520
  end
519
521
 
520
522
  # Sanitize and auto-correct frontmatter fields instead of raising on bad data.
@@ -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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.2.16"
4
+ VERSION = "1.2.18"
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; }