completion-kit 0.20.4 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e49a3e185722be44be75a0e236862942841720af9f8b6bdcfe233778968d26ab
4
- data.tar.gz: 1545324b88bc8eef05f507d71a80e06804d4a9d3c65f99010b76610657ffa021
3
+ metadata.gz: 5db8eacad4c6487a87d970c4b942657ad5033b0647a846bdfba2a7c9548a54b0
4
+ data.tar.gz: 956456ed6c96d122283c6a437a91c062323153bee1e53ec32d10bae56bf2636c
5
5
  SHA512:
6
- metadata.gz: 21f1b48c9ed2ba23b111eb1ff733ac62f180050ce3e1edd9ea31719ee25cf557025af86e00f13be27b6e04a79afb677ced23fa7e7e5a9580715249533559338c
7
- data.tar.gz: a7799c294e109c42a585f9fc7aa01e94d389291dd3fc1eec6821123ea4cb066e5cb805b803dfcf935f771b494bf1324fd7f5787f781746cfd0290d6c97b93641
6
+ metadata.gz: e8b1f1e18e33a4875944bd779e107280b83acfc231cc4940b4f6fdcdd30d17d093c046fc0fb5836c2675c728681277006ccc54d9d0d72886ac09117d7c924c0d
7
+ data.tar.gz: c82ff9429c534d82ec3ca7b0acdcfa8ec1d50dfea091b4b04167a3dc65289b06bd3f4a5e2168654cbfb2a127fdd2fac69e77168847d899edd22b136e382e2560
@@ -296,3 +296,75 @@ document.addEventListener("click", function(e) {
296
296
  btn.textContent = "Applied ✓";
297
297
  field.focus({ preventScroll: true });
298
298
  });
299
+
300
+ function ckSelectClose(sel) {
301
+ if (!sel.classList.contains("is-open")) return;
302
+ sel.classList.remove("is-open");
303
+ var menu = sel.querySelector("[data-ck-select-menu]");
304
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
305
+ if (menu) menu.hidden = true;
306
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
307
+ }
308
+
309
+ function ckSelectOpen(sel) {
310
+ document.querySelectorAll("[data-ck-select].is-open").forEach(ckSelectClose);
311
+ sel.classList.add("is-open");
312
+ var menu = sel.querySelector("[data-ck-select-menu]");
313
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
314
+ if (menu) menu.hidden = false;
315
+ if (trigger) trigger.setAttribute("aria-expanded", "true");
316
+ var current = menu.querySelector('[aria-selected="true"]') || menu.querySelector('[role="option"]');
317
+ if (current) current.focus();
318
+ }
319
+
320
+ function ckSelectChoose(sel, option) {
321
+ var input = sel.querySelector("[data-ck-select-value]");
322
+ var label = sel.querySelector("[data-ck-select-label]");
323
+ var text = option.querySelector("[data-ck-select-text]");
324
+ sel.querySelectorAll('[role="option"]').forEach(function(o) {
325
+ o.setAttribute("aria-selected", o === option ? "true" : "false");
326
+ });
327
+ input.value = option.getAttribute("data-value");
328
+ if (text) label.textContent = text.textContent;
329
+ input.dispatchEvent(new Event("change", { bubbles: true }));
330
+ ckSelectClose(sel);
331
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
332
+ if (trigger) trigger.focus();
333
+ }
334
+
335
+ document.addEventListener("click", function(e) {
336
+ var trigger = e.target.closest("[data-ck-select-trigger]");
337
+ if (trigger) {
338
+ var sel = trigger.closest("[data-ck-select]");
339
+ if (sel.classList.contains("is-open")) { ckSelectClose(sel); } else { ckSelectOpen(sel); }
340
+ return;
341
+ }
342
+ var option = e.target.closest('[data-ck-select] [role="option"]');
343
+ if (option) {
344
+ ckSelectChoose(option.closest("[data-ck-select]"), option);
345
+ return;
346
+ }
347
+ document.querySelectorAll("[data-ck-select].is-open").forEach(function(sel) {
348
+ if (!sel.contains(e.target)) ckSelectClose(sel);
349
+ });
350
+ });
351
+
352
+ document.addEventListener("keydown", function(e) {
353
+ var sel = e.target.closest("[data-ck-select]");
354
+ if (!sel) return;
355
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
356
+ var options = Array.prototype.slice.call(sel.querySelectorAll('[role="option"]'));
357
+ var open = sel.classList.contains("is-open");
358
+ if (e.target === trigger && !open) {
359
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); ckSelectOpen(sel); }
360
+ return;
361
+ }
362
+ if (!open) return;
363
+ var idx = options.indexOf(document.activeElement);
364
+ if (e.key === "ArrowDown") { e.preventDefault(); (options[idx + 1] || options[0]).focus(); }
365
+ else if (e.key === "ArrowUp") { e.preventDefault(); (options[idx - 1] || options[options.length - 1]).focus(); }
366
+ else if (e.key === "Home") { e.preventDefault(); options[0].focus(); }
367
+ else if (e.key === "End") { e.preventDefault(); options[options.length - 1].focus(); }
368
+ else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (options[idx]) ckSelectChoose(sel, options[idx]); }
369
+ else if (e.key === "Escape") { e.preventDefault(); ckSelectClose(sel); trigger.focus(); }
370
+ });
@@ -1981,6 +1981,117 @@ label.ck-checkbox input {
1981
1981
  color: var(--ck-text);
1982
1982
  }
1983
1983
 
1984
+ .ck-select {
1985
+ position: relative;
1986
+ }
1987
+
1988
+ .ck-select__trigger {
1989
+ width: 100%;
1990
+ display: flex;
1991
+ align-items: center;
1992
+ justify-content: space-between;
1993
+ gap: 0.5rem;
1994
+ padding: 0.65rem 0.85rem;
1995
+ border: 1px solid var(--ck-line-strong);
1996
+ border-radius: var(--ck-radius);
1997
+ background: var(--ck-bg);
1998
+ color: var(--ck-text);
1999
+ font-family: var(--ck-mono);
2000
+ font-size: 0.9rem;
2001
+ text-align: left;
2002
+ cursor: pointer;
2003
+ transition: border-color 0.15s, box-shadow 0.15s;
2004
+ }
2005
+
2006
+ .ck-select__label {
2007
+ overflow: hidden;
2008
+ text-overflow: ellipsis;
2009
+ white-space: nowrap;
2010
+ }
2011
+
2012
+ .ck-select__trigger:focus-visible,
2013
+ .ck-select.is-open .ck-select__trigger {
2014
+ outline: none;
2015
+ border-color: var(--ck-accent);
2016
+ box-shadow: 0 0 0 3px var(--ck-accent-soft);
2017
+ }
2018
+
2019
+ .ck-select__chevron {
2020
+ display: inline-flex;
2021
+ flex-shrink: 0;
2022
+ color: var(--ck-muted);
2023
+ transition: transform 0.15s;
2024
+ }
2025
+
2026
+ .ck-select__chevron svg {
2027
+ width: 1rem;
2028
+ height: 1rem;
2029
+ }
2030
+
2031
+ .ck-select.is-open .ck-select__chevron {
2032
+ transform: rotate(180deg);
2033
+ }
2034
+
2035
+ .ck-select__menu {
2036
+ position: absolute;
2037
+ z-index: 30;
2038
+ left: 0;
2039
+ right: 0;
2040
+ top: calc(100% + 0.35rem);
2041
+ margin: 0;
2042
+ padding: 0.3rem;
2043
+ list-style: none;
2044
+ background: var(--ck-surface);
2045
+ border: 1px solid var(--ck-line-strong);
2046
+ border-radius: var(--ck-radius-lg);
2047
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
2048
+ max-height: 18rem;
2049
+ overflow-y: auto;
2050
+ font-family: var(--ck-mono);
2051
+ }
2052
+
2053
+ .ck-select__option {
2054
+ display: flex;
2055
+ align-items: center;
2056
+ gap: 0.5rem;
2057
+ padding: 0.5rem 0.6rem;
2058
+ border-radius: var(--ck-radius);
2059
+ color: var(--ck-text);
2060
+ font-size: 0.9rem;
2061
+ cursor: pointer;
2062
+ }
2063
+
2064
+ .ck-select__option:hover,
2065
+ .ck-select__option:focus-visible {
2066
+ outline: none;
2067
+ background: var(--ck-surface-hover);
2068
+ }
2069
+
2070
+ .ck-select__option[aria-selected="true"] {
2071
+ color: var(--ck-accent);
2072
+ }
2073
+
2074
+ .ck-select__tick {
2075
+ display: inline-flex;
2076
+ flex-shrink: 0;
2077
+ width: 1rem;
2078
+ color: var(--ck-accent);
2079
+ opacity: 0;
2080
+ }
2081
+
2082
+ .ck-select__tick svg {
2083
+ width: 1rem;
2084
+ height: 1rem;
2085
+ }
2086
+
2087
+ .ck-select__option[aria-selected="true"] .ck-select__tick {
2088
+ opacity: 1;
2089
+ }
2090
+
2091
+ .ck-select__text {
2092
+ flex: 1;
2093
+ }
2094
+
1984
2095
  .ck-section-title {
1985
2096
  font-size: 1.05rem;
1986
2097
  font-weight: 600;
@@ -169,6 +169,48 @@ module CompletionKit
169
169
  end
170
170
  end
171
171
 
172
+ CHECK_KIND_LABELS = {
173
+ "contains" => "Contains a phrase",
174
+ "not_contains" => "Does not contain a phrase",
175
+ "equals" => "Equals exactly",
176
+ "regex" => "Matches a pattern",
177
+ "valid_json" => "Is valid JSON",
178
+ "json_path_equals" => "A JSON field equals a value",
179
+ "length_bounds" => "Length is within a range",
180
+ "no_refusal" => "Is not a refusal"
181
+ }.freeze
182
+
183
+ CHECK_TARGET_LABELS = {
184
+ "response_text" => "The response text",
185
+ "input_data" => "The input row",
186
+ "json_path" => "A value from the response JSON"
187
+ }.freeze
188
+
189
+ CHECK_FIELD_LABELS = {
190
+ "value" => "Text to look for",
191
+ "pattern" => "Pattern",
192
+ "json_path" => "JSON path",
193
+ "expected" => "Expected value",
194
+ "target_path" => "Path into the JSON",
195
+ "min" => "Shortest allowed",
196
+ "max" => "Longest allowed",
197
+ "case_sensitive" => "Case sensitive",
198
+ "multiline" => "Multiline",
199
+ "trim" => "Trim whitespace"
200
+ }.freeze
201
+
202
+ def ck_check_kind_label(kind)
203
+ CHECK_KIND_LABELS.fetch(kind.to_s) { kind.to_s.humanize }
204
+ end
205
+
206
+ def ck_check_target_label(target)
207
+ CHECK_TARGET_LABELS.fetch(target.to_s) { target.to_s.humanize }
208
+ end
209
+
210
+ def ck_check_field_label(key)
211
+ CHECK_FIELD_LABELS.fetch(key.to_s) { key.to_s.humanize }
212
+ end
213
+
172
214
  def ck_result_change_badge(change)
173
215
  case change
174
216
  when "broke"
@@ -1,16 +1,16 @@
1
1
  <% config = local_assigns.fetch(:config) %>
2
2
  <dl class="ck-check-spec">
3
3
  <div class="ck-check-spec__row">
4
- <dt class="ck-check-spec__term">Kind</dt>
5
- <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= config["check_kind"] %></code></dd>
4
+ <dt class="ck-check-spec__term">Rule</dt>
5
+ <dd class="ck-check-spec__val"><%= ck_check_kind_label(config["check_kind"]) %></dd>
6
6
  </div>
7
7
  <div class="ck-check-spec__row">
8
- <dt class="ck-check-spec__term">Target</dt>
9
- <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= config["target"] %></code></dd>
8
+ <dt class="ck-check-spec__term">Checks</dt>
9
+ <dd class="ck-check-spec__val"><%= ck_check_target_label(config["target"]) %></dd>
10
10
  </div>
11
11
  <% config.except("check_kind", "target").each do |key, value| %>
12
12
  <div class="ck-check-spec__row">
13
- <dt class="ck-check-spec__term"><%= key.humanize %></dt>
13
+ <dt class="ck-check-spec__term"><%= ck_check_field_label(key) %></dt>
14
14
  <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= value %></code></dd>
15
15
  </div>
16
16
  <% end %>
@@ -159,63 +159,62 @@
159
159
  <% check = metric.check_config || {} %>
160
160
  <div class="ck-field ck-field--spacious" data-ck-metric-editor="check" <%= "hidden" unless metric.check? %>>
161
161
  <p class="ck-section-title">Check</p>
162
- <p class="ck-hint">A deterministic pass/fail rule. Fill only the fields the chosen kind needs.</p>
162
+ <p class="ck-hint">A pass or fail rule with no AI and no cost. You only fill in the fields the chosen rule needs.</p>
163
163
 
164
164
  <div class="ck-field">
165
- <label class="ck-label" for="metric_check_kind">Check kind</label>
166
- <select name="metric[check_config][check_kind]" id="metric_check_kind" class="ck-input">
167
- <% CompletionKit::Checks::Registry.kinds.each do |kind| %>
168
- <option value="<%= kind %>"<%= " selected" if check["check_kind"] == kind %>><%= kind %></option>
169
- <% end %>
170
- </select>
165
+ <p class="ck-label" id="metric_check_kind_label">Rule</p>
166
+ <%= render "completion_kit/shared/branded_select",
167
+ name: "metric[check_config][check_kind]",
168
+ options: CompletionKit::Checks::Registry.kinds.map { |kind| [kind, ck_check_kind_label(kind)] },
169
+ selected: check["check_kind"] || CompletionKit::Checks::Registry.kinds.first,
170
+ labelledby: "metric_check_kind_label" %>
171
171
  </div>
172
172
 
173
173
  <div class="ck-field">
174
- <label class="ck-label" for="metric_check_target">Target</label>
175
- <select name="metric[check_config][target]" id="metric_check_target" class="ck-input">
176
- <% CompletionKit::Checks::TargetResolver::TARGETS.each do |target| %>
177
- <option value="<%= target %>"<%= " selected" if check["target"] == target %>><%= target %></option>
178
- <% end %>
179
- </select>
174
+ <p class="ck-label" id="metric_check_target_label">What to check</p>
175
+ <%= render "completion_kit/shared/branded_select",
176
+ name: "metric[check_config][target]",
177
+ options: CompletionKit::Checks::TargetResolver::TARGETS.map { |target| [target, ck_check_target_label(target)] },
178
+ selected: check["target"] || CompletionKit::Checks::TargetResolver::TARGETS.first,
179
+ labelledby: "metric_check_target_label" %>
180
180
  </div>
181
181
 
182
182
  <div class="ck-field" data-ck-check-field="target_path">
183
- <label class="ck-label" for="metric_check_target_path">Target path</label>
184
- <p class="ck-hint">Used when target is json_path, e.g. data.items.0.name.</p>
183
+ <label class="ck-label" for="metric_check_target_path">Path into the JSON</label>
184
+ <p class="ck-hint">Where to look inside the response, like data.items.0.name.</p>
185
185
  <input type="text" name="metric[check_config][target_path]" id="metric_check_target_path" class="ck-input" value="<%= check["target_path"] %>">
186
186
  </div>
187
187
 
188
188
  <div class="ck-field" data-ck-check-field="value">
189
- <label class="ck-label" for="metric_check_value">Value</label>
190
- <p class="ck-hint">The substring or exact string for contains, not_contains, or equals.</p>
189
+ <label class="ck-label" for="metric_check_value">Text to look for</label>
191
190
  <input type="text" name="metric[check_config][value]" id="metric_check_value" class="ck-input" value="<%= check["value"] %>">
192
191
  </div>
193
192
 
194
193
  <div class="ck-field" data-ck-check-field="pattern">
195
194
  <label class="ck-label" for="metric_check_pattern">Pattern</label>
196
- <p class="ck-hint">A regular expression for the regex kind.</p>
195
+ <p class="ck-hint">A regular expression to match against.</p>
197
196
  <input type="text" name="metric[check_config][pattern]" id="metric_check_pattern" class="ck-input" value="<%= check["pattern"] %>">
198
197
  </div>
199
198
 
200
199
  <div class="ck-field" data-ck-check-field="json_path">
201
200
  <label class="ck-label" for="metric_check_json_path">JSON path</label>
202
- <p class="ck-hint">Dotted path into parsed JSON for json_path_equals.</p>
201
+ <p class="ck-hint">The path to the value inside the response, like data.status.</p>
203
202
  <input type="text" name="metric[check_config][json_path]" id="metric_check_json_path" class="ck-input" value="<%= check["json_path"] %>">
204
203
  </div>
205
204
 
206
205
  <div class="ck-field" data-ck-check-field="expected">
207
- <label class="ck-label" for="metric_check_expected">Expected</label>
208
- <p class="ck-hint">The value the JSON path must equal.</p>
206
+ <label class="ck-label" for="metric_check_expected">Expected value</label>
207
+ <p class="ck-hint">The value it should equal.</p>
209
208
  <input type="text" name="metric[check_config][expected]" id="metric_check_expected" class="ck-input" value="<%= check["expected"] %>">
210
209
  </div>
211
210
 
212
211
  <div class="ck-field-row">
213
212
  <div class="ck-field" data-ck-check-field="min">
214
- <label class="ck-label" for="metric_check_min">Min length</label>
213
+ <label class="ck-label" for="metric_check_min">Shortest allowed</label>
215
214
  <input type="number" name="metric[check_config][min]" id="metric_check_min" class="ck-input" value="<%= check["min"] %>">
216
215
  </div>
217
216
  <div class="ck-field" data-ck-check-field="max">
218
- <label class="ck-label" for="metric_check_max">Max length</label>
217
+ <label class="ck-label" for="metric_check_max">Longest allowed</label>
219
218
  <input type="number" name="metric[check_config][max]" id="metric_check_max" class="ck-input" value="<%= check["max"] %>">
220
219
  </div>
221
220
  </div>
@@ -0,0 +1,18 @@
1
+ <% chosen = options.find { |value, _label| value.to_s == selected.to_s } || options.first %>
2
+ <div class="ck-select" data-ck-select>
3
+ <input type="hidden" name="<%= name %>" value="<%= chosen && chosen.first %>" data-ck-select-value>
4
+ <button type="button" class="ck-select__trigger" data-ck-select-trigger
5
+ aria-haspopup="listbox" aria-expanded="false" aria-labelledby="<%= labelledby %>">
6
+ <span class="ck-select__label" data-ck-select-label><%= chosen && chosen.last %></span>
7
+ <span class="ck-select__chevron" aria-hidden="true"><%= heroicon_tag "chevron-down" %></span>
8
+ </button>
9
+ <ul class="ck-select__menu" role="listbox" hidden data-ck-select-menu aria-labelledby="<%= labelledby %>">
10
+ <% options.each do |value, label| %>
11
+ <li class="ck-select__option" role="option" tabindex="-1" data-value="<%= value %>"
12
+ aria-selected="<%= chosen && chosen.first.to_s == value.to_s ? "true" : "false" %>">
13
+ <span class="ck-select__tick" aria-hidden="true"><%= heroicon_tag "check" %></span>
14
+ <span class="ck-select__text" data-ck-select-text><%= label %></span>
15
+ </li>
16
+ <% end %>
17
+ </ul>
18
+ </div>
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.20.4"
2
+ VERSION = "0.21.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completion-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.4
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -419,6 +419,7 @@ files:
419
419
  - app/views/completion_kit/runs/index.html.erb
420
420
  - app/views/completion_kit/runs/new.html.erb
421
421
  - app/views/completion_kit/runs/show.html.erb
422
+ - app/views/completion_kit/shared/_branded_select.html.erb
422
423
  - app/views/completion_kit/shared/_settings_nav.html.erb
423
424
  - app/views/completion_kit/suggestions/_scoreboard.html.erb
424
425
  - app/views/completion_kit/suggestions/_state.html.erb