openclacky 0.7.7 → 0.7.8
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/.clacky/skills/commit/SKILL.md +2 -2
- data/CHANGELOG.md +21 -0
- data/lib/clacky/agent/tool_executor.rb +0 -12
- data/lib/clacky/agent.rb +0 -6
- data/lib/clacky/agent_config.rb +1 -5
- data/lib/clacky/cli.rb +1 -2
- data/lib/clacky/server/http_server.rb +48 -14
- data/lib/clacky/server/scheduler.rb +2 -1
- data/lib/clacky/skill.rb +7 -0
- data/lib/clacky/skill_loader.rb +28 -0
- data/lib/clacky/ui2/components/input_area.rb +0 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +242 -0
- data/lib/clacky/web/app.js +214 -64
- data/lib/clacky/web/index.html +32 -0
- data/lib/clacky/web/sessions.js +26 -42
- data/lib/clacky/web/skills.js +286 -0
- data/lib/clacky/web/tasks.js +28 -65
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b2a72212b8b9e8072acb09a055df1afdd8225ee81de41f11dce460a68ff1781
|
|
4
|
+
data.tar.gz: e8048bb38dc4a8ae8c85937165e10a855f8642d6759c8a2a94aa336b084e4188
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '095710e060b7c29882eb128be1a4008281435c13af07e7da01ec9ad1af27c30e252eb19e77f557882628409058d504f29024ea0dbb396ed20d326b26cfe1925c'
|
|
7
|
+
data.tar.gz: 2875ac5911322341be5fe396ad44a98e4cbcea51c95ed7d832e3978d5790542ca94ae0f065f6170484d9900be2f1ca190f6db0d25c14a33366f924f9d13a147d
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
+
---
|
|
2
3
|
name: commit
|
|
3
4
|
description: Smart Git commit helper that analyzes changes and creates semantic commits
|
|
4
|
-
disable-model-invocation: false
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -472,4 +472,4 @@ This skill works best:
|
|
|
472
472
|
|
|
473
473
|
- Created: 2025-02-01
|
|
474
474
|
- Purpose: Improve commit quality and development workflow
|
|
475
|
-
- Compatible with: Any git repository
|
|
475
|
+
- Compatible with: Any git repository
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.8] - 2026-03-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Skills panel in web UI: list all skills, enable/disable with toggle, view skill details
|
|
14
|
+
- Hash-based routing (`#session/:id`, `#tasks`, `#skills`, `#settings`) with deep-link and refresh support
|
|
15
|
+
- REST API endpoints for skills management (`GET /api/skills`, `PATCH /api/skills/:name/toggle`)
|
|
16
|
+
- `disabled?` helper on `Skill` model for quick enabled/disabled state checks
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
- Centralized `Router` object in web UI — single source of truth for all panel switching and sidebar highlight state
|
|
20
|
+
- Web UI frontend split further: `skills.js` extracted as standalone module
|
|
21
|
+
- Ctrl-C in web server now exits immediately via `StartCallback` trap override
|
|
22
|
+
- Skill enable/disable now writes `disable-model-invocation: false` (retains field) instead of deleting it
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Sidebar highlight for Tasks and Skills stuck active after navigating away
|
|
26
|
+
- Router correctly restores last view on page refresh via hash URL
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Removed `plan_only` permission mode from agent, CLI, and web UI
|
|
30
|
+
|
|
10
31
|
## [0.7.7] - 2026-03-04
|
|
11
32
|
|
|
12
33
|
### Added
|
|
@@ -16,8 +16,6 @@ module Clacky
|
|
|
16
16
|
when :confirm_safes
|
|
17
17
|
# Use SafeShell integration for safety check
|
|
18
18
|
is_safe_operation?(tool_name, tool_params)
|
|
19
|
-
when :plan_only
|
|
20
|
-
false
|
|
21
19
|
else
|
|
22
20
|
false
|
|
23
21
|
end
|
|
@@ -217,16 +215,6 @@ module Clacky
|
|
|
217
215
|
}
|
|
218
216
|
end
|
|
219
217
|
|
|
220
|
-
# Build planned result for plan-only mode
|
|
221
|
-
# @param call [Hash] Tool call
|
|
222
|
-
# @return [Hash] Formatted planned result
|
|
223
|
-
def build_planned_result(call)
|
|
224
|
-
{
|
|
225
|
-
id: call[:id],
|
|
226
|
-
content: JSON.generate({ planned: true, message: "Tool execution skipped (plan mode)" })
|
|
227
|
-
}
|
|
228
|
-
end
|
|
229
|
-
|
|
230
218
|
# Check if a tool is potentially slow and should show progress
|
|
231
219
|
# @param tool_name [String] Name of the tool
|
|
232
220
|
# @param args [Hash] Tool arguments
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -389,12 +389,6 @@ module Clacky
|
|
|
389
389
|
end
|
|
390
390
|
else
|
|
391
391
|
# Permission check (if not in auto-approve mode)
|
|
392
|
-
if @config.is_plan_only?
|
|
393
|
-
@ui&.show_info("Planned: #{call[:name]}")
|
|
394
|
-
results << build_planned_result(call)
|
|
395
|
-
next
|
|
396
|
-
end
|
|
397
|
-
|
|
398
392
|
confirmation = confirm_tool_use?(call)
|
|
399
393
|
unless confirmation[:approved]
|
|
400
394
|
# Show denial warning only for user-initiated denials (not system-injected preview errors)
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -148,7 +148,7 @@ module Clacky
|
|
|
148
148
|
# Default model for ClaudeCode environment
|
|
149
149
|
CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-5"
|
|
150
150
|
|
|
151
|
-
PERMISSION_MODES = [:auto_approve, :confirm_safes
|
|
151
|
+
PERMISSION_MODES = [:auto_approve, :confirm_safes].freeze
|
|
152
152
|
|
|
153
153
|
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
154
154
|
:enable_compression, :enable_prompt_caching,
|
|
@@ -397,10 +397,6 @@ module Clacky
|
|
|
397
397
|
true
|
|
398
398
|
end
|
|
399
399
|
|
|
400
|
-
def is_plan_only?
|
|
401
|
-
@permission_mode == :plan_only
|
|
402
|
-
end
|
|
403
|
-
|
|
404
400
|
private def validate_permission_mode(mode)
|
|
405
401
|
mode ||= :confirm_safes
|
|
406
402
|
mode = mode.to_sym
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -27,7 +27,6 @@ module Clacky
|
|
|
27
27
|
Permission modes:
|
|
28
28
|
auto_approve - Automatically execute all tools (use with caution)
|
|
29
29
|
confirm_safes - Auto-approve safe operations, confirm risky ones (default)
|
|
30
|
-
plan_only - Generate plan without executing
|
|
31
30
|
|
|
32
31
|
UI themes:
|
|
33
32
|
hacker - Matrix/hacker-style with bracket symbols (default)
|
|
@@ -42,7 +41,7 @@ module Clacky
|
|
|
42
41
|
$ clacky agent --mode=auto_approve --path /path/to/project
|
|
43
42
|
LONGDESC
|
|
44
43
|
option :mode, type: :string, default: "confirm_safes",
|
|
45
|
-
desc: "Permission mode: auto_approve, confirm_safes
|
|
44
|
+
desc: "Permission mode: auto_approve, confirm_safes"
|
|
46
45
|
option :theme, type: :string, default: "hacker",
|
|
47
46
|
desc: "UI theme: hacker, minimal (default: hacker)"
|
|
48
47
|
option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Show detailed output"
|
|
@@ -33,14 +33,19 @@ module Clacky
|
|
|
33
33
|
session_registry: @registry,
|
|
34
34
|
session_builder: method(:build_session)
|
|
35
35
|
)
|
|
36
|
+
@skill_loader = Clacky::SkillLoader.new
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def start
|
|
40
|
+
# Override WEBrick's built-in signal traps via StartCallback,
|
|
41
|
+
# which fires after WEBrick sets its own INT/TERM handlers.
|
|
42
|
+
# This ensures Ctrl-C always exits immediately.
|
|
39
43
|
server = WEBrick::HTTPServer.new(
|
|
40
44
|
BindAddress: @host,
|
|
41
45
|
Port: @port,
|
|
42
46
|
Logger: WEBrick::Log.new(File::NULL),
|
|
43
|
-
AccessLog: []
|
|
47
|
+
AccessLog: [],
|
|
48
|
+
StartCallback: proc { trap("INT") { exit(0) }; trap("TERM") { exit(0) } }
|
|
44
49
|
)
|
|
45
50
|
|
|
46
51
|
# Mount API + WebSocket handler (takes priority).
|
|
@@ -69,10 +74,6 @@ module Clacky
|
|
|
69
74
|
res["Pragma"] = "no-cache"
|
|
70
75
|
end
|
|
71
76
|
|
|
72
|
-
# Graceful shutdown on Ctrl-C
|
|
73
|
-
trap("INT") { @scheduler.stop; server.shutdown }
|
|
74
|
-
trap("TERM") { @scheduler.stop; server.shutdown }
|
|
75
|
-
|
|
76
77
|
puts "🌐 Clacky Web UI running at http://#{@host}:#{@port}"
|
|
77
78
|
puts " Press Ctrl-C to stop."
|
|
78
79
|
|
|
@@ -108,6 +109,7 @@ module Clacky
|
|
|
108
109
|
when ["GET", "/api/tasks"] then api_list_tasks(res)
|
|
109
110
|
when ["POST", "/api/tasks"] then api_create_task(req, res)
|
|
110
111
|
when ["POST", "/api/tasks/run"] then api_run_task(req, res)
|
|
112
|
+
when ["GET", "/api/skills"] then api_list_skills(res)
|
|
111
113
|
when ["GET", "/api/config"] then api_get_config(res)
|
|
112
114
|
when ["POST", "/api/config"] then api_save_config(req, res)
|
|
113
115
|
when ["POST", "/api/config/test"] then api_test_config(req, res)
|
|
@@ -125,6 +127,9 @@ module Clacky
|
|
|
125
127
|
elsif method == "DELETE" && path.start_with?("/api/tasks/")
|
|
126
128
|
name = URI.decode_www_form_component(path.sub("/api/tasks/", ""))
|
|
127
129
|
api_delete_task(name, res)
|
|
130
|
+
elsif method == "PATCH" && path.match?(%r{^/api/skills/[^/]+/toggle$})
|
|
131
|
+
name = URI.decode_www_form_component(path.sub("/api/skills/", "").sub("/toggle", ""))
|
|
132
|
+
api_toggle_skill(name, req, res)
|
|
128
133
|
else
|
|
129
134
|
not_found(res)
|
|
130
135
|
end
|
|
@@ -138,15 +143,12 @@ module Clacky
|
|
|
138
143
|
end
|
|
139
144
|
|
|
140
145
|
def api_create_session(req, res)
|
|
141
|
-
body
|
|
146
|
+
body = parse_json_body(req)
|
|
142
147
|
name = body["name"]
|
|
143
|
-
working_dir = default_working_dir
|
|
148
|
+
working_dir = body["working_dir"]&.then { |d| File.expand_path(d) } || default_working_dir
|
|
144
149
|
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
json_response(res, 422, { error: "Directory does not exist: #{working_dir}" })
|
|
148
|
-
return
|
|
149
|
-
end
|
|
150
|
+
# Auto-create the working directory if it does not exist yet
|
|
151
|
+
FileUtils.mkdir_p(working_dir)
|
|
150
152
|
|
|
151
153
|
session_id = build_session(name: name, working_dir: working_dir)
|
|
152
154
|
json_response(res, 201, { session: @registry.list.find { |s| s[:id] == session_id } })
|
|
@@ -266,6 +268,40 @@ module Clacky
|
|
|
266
268
|
end
|
|
267
269
|
end
|
|
268
270
|
|
|
271
|
+
# ── Skills API ────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
# GET /api/skills — list all loaded skills with metadata
|
|
274
|
+
def api_list_skills(res)
|
|
275
|
+
@skill_loader.load_all # refresh from disk on each request
|
|
276
|
+
skills = @skill_loader.all_skills.map do |skill|
|
|
277
|
+
source = @skill_loader.loaded_from[skill.identifier]
|
|
278
|
+
{
|
|
279
|
+
name: skill.identifier,
|
|
280
|
+
description: skill.context_description,
|
|
281
|
+
source: source,
|
|
282
|
+
enabled: !skill.disabled?
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
json_response(res, 200, { skills: skills })
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# PATCH /api/skills/:name/toggle — enable or disable a skill
|
|
289
|
+
# Body: { enabled: true/false }
|
|
290
|
+
def api_toggle_skill(name, req, res)
|
|
291
|
+
body = parse_json_body(req)
|
|
292
|
+
enabled = body["enabled"]
|
|
293
|
+
|
|
294
|
+
if enabled.nil?
|
|
295
|
+
json_response(res, 422, { error: "enabled field required" })
|
|
296
|
+
return
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
skill = @skill_loader.toggle_skill(name, enabled: enabled)
|
|
300
|
+
json_response(res, 200, { ok: true, name: skill.identifier, enabled: !skill.disabled? })
|
|
301
|
+
rescue Clacky::AgentError => e
|
|
302
|
+
json_response(res, 422, { error: e.message })
|
|
303
|
+
end
|
|
304
|
+
|
|
269
305
|
# ── Config API ────────────────────────────────────────────────────────────
|
|
270
306
|
|
|
271
307
|
# GET /api/config — return current model configurations
|
|
@@ -529,7 +565,6 @@ module Clacky
|
|
|
529
565
|
broadcast_session_update(session_id)
|
|
530
566
|
broadcast(session_id, { type: "error", session_id: session_id, message: e.message })
|
|
531
567
|
end
|
|
532
|
-
|
|
533
568
|
@registry.with_session(session_id) { |s| s[:thread] = thread }
|
|
534
569
|
end
|
|
535
570
|
|
|
@@ -579,7 +614,6 @@ module Clacky
|
|
|
579
614
|
broadcast_session_update(session_id)
|
|
580
615
|
broadcast(session_id, { type: "error", session_id: session_id, message: e.message })
|
|
581
616
|
end
|
|
582
|
-
|
|
583
617
|
@registry.with_session(session_id) { |s| s[:thread] = thread }
|
|
584
618
|
end
|
|
585
619
|
|
|
@@ -158,7 +158,7 @@ module Clacky
|
|
|
158
158
|
|
|
159
159
|
$stdout.puts "[Clacky Scheduler] Firing task '#{task_name}' (session: #{session_id})"
|
|
160
160
|
|
|
161
|
-
# Run the agent in a background thread so the scheduler tick is non-blocking
|
|
161
|
+
# Run the agent in a background thread so the scheduler tick is non-blocking.
|
|
162
162
|
Thread.new do
|
|
163
163
|
session = @registry.get(session_id)
|
|
164
164
|
agent = nil
|
|
@@ -174,6 +174,7 @@ module Clacky
|
|
|
174
174
|
@registry.update(session_id, status: :error, error: e.message)
|
|
175
175
|
$stderr.puts "[Clacky Scheduler] Task '#{task_name}' failed: #{e.message}"
|
|
176
176
|
end
|
|
177
|
+
|
|
177
178
|
rescue => e
|
|
178
179
|
$stderr.puts "[Clacky Scheduler] Failed to fire task '#{schedule["task"]}': #{e.message}"
|
|
179
180
|
end
|
data/lib/clacky/skill.rb
CHANGED
|
@@ -30,6 +30,13 @@ module Clacky
|
|
|
30
30
|
attr_reader :allowed_tools, :context, :agent_type, :argument_hint, :hooks
|
|
31
31
|
attr_reader :fork_agent, :model, :forbidden_tools, :auto_summarize
|
|
32
32
|
|
|
33
|
+
# Check if this skill is disabled (disable-model-invocation: true)
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def disabled?
|
|
36
|
+
@disable_model_invocation == true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
33
40
|
# @param directory [Pathname, String] Path to the skill directory
|
|
34
41
|
# @param source_path [Pathname, String, nil] Optional source path for priority resolution
|
|
35
42
|
def initialize(directory, source_path: nil)
|
data/lib/clacky/skill_loader.rb
CHANGED
|
@@ -208,6 +208,34 @@ module Clacky
|
|
|
208
208
|
load_single_skill(skill_dir, skill_dir, name, source_type)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
+
# Toggle a skill's disable-model-invocation field in its SKILL.md.
|
|
212
|
+
# System skills (source: :default) cannot be toggled — raises AgentError.
|
|
213
|
+
# @param name [String] Skill identifier
|
|
214
|
+
# @param enabled [Boolean] true = enable, false = disable
|
|
215
|
+
# @return [Skill] The reloaded skill
|
|
216
|
+
def toggle_skill(name, enabled:)
|
|
217
|
+
skill = @skills[name]
|
|
218
|
+
raise Clacky::AgentError, "Skill not found: #{name}" unless skill
|
|
219
|
+
raise Clacky::AgentError, "Cannot toggle system skill: #{name}" if @loaded_from[name] == :default
|
|
220
|
+
|
|
221
|
+
skill_file = skill.directory.join("SKILL.md")
|
|
222
|
+
fm = (skill.frontmatter || {}).dup
|
|
223
|
+
|
|
224
|
+
if enabled
|
|
225
|
+
fm["disable-model-invocation"] = false
|
|
226
|
+
else
|
|
227
|
+
fm["disable-model-invocation"] = true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
skill_file.write(build_skill_content(fm, skill.content))
|
|
231
|
+
|
|
232
|
+
# Reload into registry
|
|
233
|
+
reloaded = Skill.new(skill.directory, source_path: skill.source_path)
|
|
234
|
+
@skills[reloaded.identifier] = reloaded
|
|
235
|
+
@skills_by_command[reloaded.slash_command] = reloaded
|
|
236
|
+
reloaded
|
|
237
|
+
end
|
|
238
|
+
|
|
211
239
|
# Delete a skill
|
|
212
240
|
# @param name [String] Skill name
|
|
213
241
|
# @return [Boolean] True if deleted, false if not found
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -779,6 +779,248 @@ body {
|
|
|
779
779
|
.form-textarea:focus { outline: none; border-color: #58a6ff; }
|
|
780
780
|
.form-hint { font-size: 11px; color: #8b949e; margin-top: 4px; }
|
|
781
781
|
|
|
782
|
+
/* ── Skills Panel ────────────────────────────────────────────────────────── */
|
|
783
|
+
#skills-panel {
|
|
784
|
+
display: flex;
|
|
785
|
+
flex-direction: column;
|
|
786
|
+
height: 100%;
|
|
787
|
+
overflow: hidden;
|
|
788
|
+
}
|
|
789
|
+
#skills-header {
|
|
790
|
+
padding: 12px 20px;
|
|
791
|
+
border-bottom: 1px solid #30363d;
|
|
792
|
+
display: flex;
|
|
793
|
+
align-items: center;
|
|
794
|
+
justify-content: space-between;
|
|
795
|
+
background: #161b22;
|
|
796
|
+
flex-shrink: 0;
|
|
797
|
+
font-weight: 600;
|
|
798
|
+
font-size: 15px;
|
|
799
|
+
}
|
|
800
|
+
#btn-create-skill {
|
|
801
|
+
padding: 5px 12px;
|
|
802
|
+
background: #238636;
|
|
803
|
+
color: #fff;
|
|
804
|
+
border: none;
|
|
805
|
+
border-radius: 6px;
|
|
806
|
+
font-size: 13px;
|
|
807
|
+
cursor: pointer;
|
|
808
|
+
white-space: nowrap;
|
|
809
|
+
}
|
|
810
|
+
#btn-create-skill:hover { background: #2ea043; }
|
|
811
|
+
#skills-body {
|
|
812
|
+
flex: 1;
|
|
813
|
+
overflow: hidden;
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
padding: 20px 24px;
|
|
817
|
+
gap: 16px;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/* ── Skills Tabs ─────────────────────────────────────────────────────────── */
|
|
821
|
+
#skills-tabs {
|
|
822
|
+
display: flex;
|
|
823
|
+
gap: 4px;
|
|
824
|
+
border-bottom: 1px solid #30363d;
|
|
825
|
+
flex-shrink: 0;
|
|
826
|
+
}
|
|
827
|
+
.skills-tab {
|
|
828
|
+
background: none;
|
|
829
|
+
border: none;
|
|
830
|
+
border-bottom: 2px solid transparent;
|
|
831
|
+
color: #8b949e;
|
|
832
|
+
cursor: pointer;
|
|
833
|
+
font-size: 13px;
|
|
834
|
+
font-weight: 500;
|
|
835
|
+
padding: 8px 16px;
|
|
836
|
+
margin-bottom: -1px;
|
|
837
|
+
transition: color .15s, border-color .15s;
|
|
838
|
+
}
|
|
839
|
+
.skills-tab:hover { color: #e6edf3; }
|
|
840
|
+
.skills-tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }
|
|
841
|
+
|
|
842
|
+
.skills-tab-content {
|
|
843
|
+
flex: 1;
|
|
844
|
+
overflow-y: auto;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/* ── My Skills list ──────────────────────────────────────────────────────── */
|
|
848
|
+
#skills-list {
|
|
849
|
+
display: flex;
|
|
850
|
+
flex-direction: column;
|
|
851
|
+
gap: 8px;
|
|
852
|
+
padding-top: 12px;
|
|
853
|
+
}
|
|
854
|
+
.skills-empty {
|
|
855
|
+
color: #8b949e;
|
|
856
|
+
font-size: 13px;
|
|
857
|
+
padding: 20px 0;
|
|
858
|
+
text-align: center;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/* ── Skill Card ──────────────────────────────────────────────────────────── */
|
|
862
|
+
.skill-card {
|
|
863
|
+
background: #161b22;
|
|
864
|
+
border: 1px solid #30363d;
|
|
865
|
+
border-radius: 8px;
|
|
866
|
+
padding: 14px 16px;
|
|
867
|
+
transition: border-color .15s;
|
|
868
|
+
}
|
|
869
|
+
.skill-card:hover { border-color: #484f58; }
|
|
870
|
+
.skill-card-main {
|
|
871
|
+
display: flex;
|
|
872
|
+
align-items: center;
|
|
873
|
+
gap: 12px;
|
|
874
|
+
}
|
|
875
|
+
.skill-card-info { flex: 1; min-width: 0; }
|
|
876
|
+
.skill-card-title {
|
|
877
|
+
display: flex;
|
|
878
|
+
align-items: center;
|
|
879
|
+
gap: 8px;
|
|
880
|
+
margin-bottom: 4px;
|
|
881
|
+
}
|
|
882
|
+
.skill-name {
|
|
883
|
+
font-size: 13px;
|
|
884
|
+
font-weight: 600;
|
|
885
|
+
color: #e6edf3;
|
|
886
|
+
}
|
|
887
|
+
.skill-card-desc {
|
|
888
|
+
font-size: 12px;
|
|
889
|
+
color: #8b949e;
|
|
890
|
+
line-height: 1.5;
|
|
891
|
+
white-space: nowrap;
|
|
892
|
+
overflow: hidden;
|
|
893
|
+
text-overflow: ellipsis;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/* ── Skill Badges ────────────────────────────────────────────────────────── */
|
|
897
|
+
.skill-badge {
|
|
898
|
+
font-size: 10px;
|
|
899
|
+
font-weight: 600;
|
|
900
|
+
padding: 2px 7px;
|
|
901
|
+
border-radius: 10px;
|
|
902
|
+
letter-spacing: 0.3px;
|
|
903
|
+
flex-shrink: 0;
|
|
904
|
+
}
|
|
905
|
+
.skill-badge-system {
|
|
906
|
+
background: #1f3a5f;
|
|
907
|
+
color: #79c0ff;
|
|
908
|
+
border: 1px solid #1d4070;
|
|
909
|
+
}
|
|
910
|
+
.skill-badge-custom {
|
|
911
|
+
background: #1f3024;
|
|
912
|
+
color: #56d364;
|
|
913
|
+
border: 1px solid #1c4028;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/* ── Toggle Switch ───────────────────────────────────────────────────────── */
|
|
917
|
+
.skill-toggle {
|
|
918
|
+
position: relative;
|
|
919
|
+
display: inline-flex;
|
|
920
|
+
align-items: center;
|
|
921
|
+
cursor: pointer;
|
|
922
|
+
flex-shrink: 0;
|
|
923
|
+
}
|
|
924
|
+
.skill-toggle-disabled {
|
|
925
|
+
cursor: not-allowed;
|
|
926
|
+
opacity: 0.4;
|
|
927
|
+
}
|
|
928
|
+
.skill-toggle-input {
|
|
929
|
+
opacity: 0;
|
|
930
|
+
width: 0;
|
|
931
|
+
height: 0;
|
|
932
|
+
position: absolute;
|
|
933
|
+
}
|
|
934
|
+
.skill-toggle-track {
|
|
935
|
+
display: inline-block;
|
|
936
|
+
width: 36px;
|
|
937
|
+
height: 20px;
|
|
938
|
+
background: #30363d;
|
|
939
|
+
border-radius: 10px;
|
|
940
|
+
transition: background .2s;
|
|
941
|
+
position: relative;
|
|
942
|
+
}
|
|
943
|
+
.skill-toggle-track::after {
|
|
944
|
+
content: "";
|
|
945
|
+
position: absolute;
|
|
946
|
+
width: 14px;
|
|
947
|
+
height: 14px;
|
|
948
|
+
background: #fff;
|
|
949
|
+
border-radius: 50%;
|
|
950
|
+
top: 3px;
|
|
951
|
+
left: 3px;
|
|
952
|
+
transition: left .2s;
|
|
953
|
+
}
|
|
954
|
+
.skill-toggle-input:checked + .skill-toggle-track {
|
|
955
|
+
background: #238636;
|
|
956
|
+
}
|
|
957
|
+
.skill-toggle-input:checked + .skill-toggle-track::after {
|
|
958
|
+
left: 19px;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/* ── Store Grid ──────────────────────────────────────────────────────────── */
|
|
962
|
+
#skills-store-grid {
|
|
963
|
+
display: flex;
|
|
964
|
+
flex-direction: column;
|
|
965
|
+
gap: 10px;
|
|
966
|
+
padding-top: 12px;
|
|
967
|
+
}
|
|
968
|
+
.store-card {
|
|
969
|
+
background: #161b22;
|
|
970
|
+
border: 1px solid #30363d;
|
|
971
|
+
border-radius: 8px;
|
|
972
|
+
padding: 16px;
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 14px;
|
|
976
|
+
transition: border-color .15s;
|
|
977
|
+
}
|
|
978
|
+
.store-card:hover { border-color: #484f58; }
|
|
979
|
+
.store-card-icon {
|
|
980
|
+
font-size: 28px;
|
|
981
|
+
flex-shrink: 0;
|
|
982
|
+
width: 44px;
|
|
983
|
+
height: 44px;
|
|
984
|
+
display: flex;
|
|
985
|
+
align-items: center;
|
|
986
|
+
justify-content: center;
|
|
987
|
+
background: #0d1117;
|
|
988
|
+
border-radius: 8px;
|
|
989
|
+
}
|
|
990
|
+
.store-card-body { flex: 1; min-width: 0; }
|
|
991
|
+
.store-card-title {
|
|
992
|
+
font-size: 13px;
|
|
993
|
+
font-weight: 600;
|
|
994
|
+
color: #e6edf3;
|
|
995
|
+
margin-bottom: 4px;
|
|
996
|
+
}
|
|
997
|
+
.store-card-desc {
|
|
998
|
+
font-size: 12px;
|
|
999
|
+
color: #8b949e;
|
|
1000
|
+
line-height: 1.5;
|
|
1001
|
+
}
|
|
1002
|
+
.store-card-actions { flex-shrink: 0; }
|
|
1003
|
+
.btn-store-install {
|
|
1004
|
+
background: #238636;
|
|
1005
|
+
color: #fff;
|
|
1006
|
+
border: none;
|
|
1007
|
+
border-radius: 6px;
|
|
1008
|
+
padding: 6px 14px;
|
|
1009
|
+
font-size: 12px;
|
|
1010
|
+
font-weight: 600;
|
|
1011
|
+
cursor: pointer;
|
|
1012
|
+
transition: background .15s;
|
|
1013
|
+
}
|
|
1014
|
+
.btn-store-install:hover { background: #2ea043; }
|
|
1015
|
+
.store-badge-installed {
|
|
1016
|
+
font-size: 12px;
|
|
1017
|
+
color: #56d364;
|
|
1018
|
+
font-weight: 600;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/* ── Skills sidebar section ──────────────────────────────────────────────── */
|
|
1022
|
+
#skill-list-items { padding: 0 8px 8px; display: flex; flex-direction: column; gap: 2px; }
|
|
1023
|
+
|
|
782
1024
|
/* ── Scrollbar ───────────────────────────────────────────────────────────── */
|
|
783
1025
|
::-webkit-scrollbar { width: 6px; }
|
|
784
1026
|
::-webkit-scrollbar-track { background: transparent; }
|