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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/lib/clacky/agent/session_serializer.rb +5 -2
- data/lib/clacky/agent/skill_manager.rb +1 -1
- data/lib/clacky/agent.rb +6 -11
- data/lib/clacky/default_skills/channel-manager/SKILL.md +4 -2
- data/lib/clacky/default_skills/media-gen/SKILL.md +1 -0
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -0
- data/lib/clacky/media/base.rb +32 -0
- data/lib/clacky/media/dashscope.rb +243 -0
- data/lib/clacky/media/generator.rb +18 -0
- data/lib/clacky/server/channel/channel_manager.rb +115 -31
- data/lib/clacky/server/http_server.rb +25 -2
- data/lib/clacky/skill.rb +3 -1
- data/lib/clacky/telemetry.rb +20 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +305 -0
- data/lib/clacky/web/billing.js +144 -0
- data/lib/clacky/web/i18n.js +144 -1
- data/lib/clacky/web/index.html +11 -0
- data/lib/clacky/web/marked.min.js +55 -45
- data/lib/clacky/web/sessions.js +6 -1
- data/lib/clacky/web/share.js +843 -0
- data/lib/clacky/web/skills.js +5 -5
- data/lib/clacky/web/vendor/qrcode/qrcode.min.js +8 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/scripts/install.ps1 +20 -19
- metadata +5 -2
|
@@ -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 &
|
|
446
|
-
/model <n> - switch
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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, "
|
|
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
|
-
|
|
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.
|
data/lib/clacky/telemetry.rb
CHANGED
|
@@ -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?
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -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; }
|