polats commited on
Commit
3c2af73
·
verified ·
1 Parent(s): aff863f

Deploy Karate Wiener (kimodo kata maker)

Browse files
Files changed (3) hide show
  1. app.py +36 -22
  2. tabs/dojo.drawer.html +9 -4
  3. tabs/dojo.tab.js +16 -4
app.py CHANGED
@@ -250,11 +250,23 @@ APP_CSS = """
250
  .kimodo-cz-katafeat:hover:not(:disabled) { background: #7c6226 !important; color: #fff1c4 !important; }
251
  .kimodo-cz-katafeat:disabled { cursor: default; }
252
  .kimodo-cz-katafeat.featured { background: #8a6d12 !important; border-color: #d9ad33 !important; color: #fff6d6 !important; cursor: default; }
 
 
 
 
 
 
 
 
 
253
  /* Library browse controls: search filter + sort select. */
254
- .kimodo-cz-libctrls { display: flex; gap: 6px; margin-bottom: 10px; }
255
  .kimodo-cz-cfilter { flex: 1; margin-bottom: 0 !important; }
256
  .kimodo-cz-csort { flex: 0 0 auto; padding: 8px 8px; font-size: 12px; color: #f2e6ff; background: #1c1622; border: 1px solid #4a3a60; border-radius: 8px; cursor: pointer; }
257
  .kimodo-cz-csort:focus { outline: none; border-color: #a86fe0; }
 
 
 
258
  /* Card footer: upvote on the left, "★ Featured" badge pushed to the lower-right. */
259
  .kimodo-cz-cardfoot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
260
  /* Per-card upvote: ▲ + count; filled when this browser has voted. */
@@ -737,8 +749,8 @@ html, body, gradio-app, .gradio-container { height: 100%; margin: 0; overflow: h
737
  .kimodo-cz-tlcard.end .kimodo-cz-tlnode.endn { border-color: #d9776a; }
738
  .kimodo-cz-tlcard.gen { background: #1d1730; border-color: #4a3a6a; } /* saved move = darker purple */
739
  .kimodo-cz-tlcard.cz-pending { background: #232026; border-color: #4a474f; }
740
- .kimodo-cz-tlcard.cz-active { border-color: #a86fe0; box-shadow: 0 0 0 1px #a86fe0; }
741
- .kimodo-cz-tlcard.sel { box-shadow: 0 0 0 2px #c79bff; }
742
  .kimodo-cz-tlcard.cz-justgen { animation: kimodo-cz-fromgray-card .55s ease; }
743
  .kimodo-cz-pcardrow.cz-pop .kimodo-cz-tlcard { animation: kimodo-cz-pop .5s cubic-bezier(.34,1.56,.64,1) both; }
744
  .kimodo-cz-tlmeta { flex: 1; min-width: 0; }
@@ -749,7 +761,7 @@ html, body, gradio-app, .gradio-container { height: 100%; margin: 0; overflow: h
749
  .kimodo-cz-tlx { position: absolute; top: 3px; right: 5px; }
750
  .kimodo-cz-tlgrip { position: absolute; left: 0; right: 0; bottom: 0; height: 15px; display: flex; align-items: center; justify-content: center; cursor: ns-resize; touch-action: none; z-index: 5; }
751
  .kimodo-cz-tlgrip span { width: 28px; height: 3px; border-radius: 2px; background: #6a5a86; transition: background .15s ease; }
752
- .kimodo-cz-tlgrip:hover span, .kimodo-cz-tlcard.cz-active .kimodo-cz-tlgrip span { background: #a86fe0; }
753
  /* active-move controls below the timeline */
754
  .kimodo-cz-controls { margin-top: 12px; padding-top: 10px; border-top: 1px solid #2a2233; }
755
  .kimodo-cz-controlslab { font-size: 11px; font-weight: 600; color: #c9b6ea; margin-bottom: 6px; }
@@ -757,7 +769,7 @@ html, body, gradio-app, .gradio-container { height: 100%; margin: 0; overflow: h
757
  to set the length = the gap between them), GENERATE right under the end, then the
758
  collapsed refine prompt + the move/rotation numbers. ---- */
759
  .kimodo-cz-activerow { overflow: visible; }
760
- .kimodo-cz-editcard { position: relative; border: 1px solid #a86fe0; border-radius: 10px; background: #241a30; padding: 8px; box-shadow: 0 0 0 1px rgba(168,111,224,0.35); }
761
  /* A move edited after it was generated → dirty (amber) so it's clearly "needs Refresh". */
762
  .kimodo-cz-editcard.cz-dirty { border-color: #5a5a64; background: #26262c; box-shadow: 0 0 0 1px rgba(90,90,100,0.35); } /* unsaved edit = gray (like a new move) */
763
  /* Not generated yet → gray. Once generated it pulses gray→purple; selecting a generated
@@ -766,14 +778,14 @@ html, body, gradio-app, .gradio-container { height: 100%; margin: 0; overflow: h
766
  .kimodo-cz-editcard.cz-genpulse { animation: kimodo-cz-gray2purple .6s ease both; }
767
  .kimodo-cz-editcard.cz-selpulse { animation: kimodo-cz-green2purple .6s ease both; }
768
  @keyframes kimodo-cz-gray2purple {
769
- 0% { background: #26262c; border-color: #5a5a64; box-shadow: 0 0 0 0 rgba(168,111,224,0); }
770
- 45% { box-shadow: 0 0 0 5px rgba(168,111,224,0.5); transform: scale(1.02); }
771
- 100% { background: #241a30; border-color: #a86fe0; box-shadow: 0 0 0 1px rgba(168,111,224,0.35); transform: scale(1); }
772
  }
773
  @keyframes kimodo-cz-green2purple {
774
- 0% { background: #1d1730; border-color: #4a3a6a; box-shadow: 0 0 0 0 rgba(168,111,224,0); } /* from the darker-purple saved card */
775
- 45% { box-shadow: 0 0 0 5px rgba(168,111,224,0.5); transform: scale(1.02); }
776
- 100% { background: #241a30; border-color: #a86fe0; box-shadow: 0 0 0 1px rgba(168,111,224,0.35); transform: scale(1); }
777
  }
778
  .kimodo-cz-emmovelab { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: #c9b6ea; margin: 0 0 5px 2px; }
779
  .kimodo-cz-emregion { position: relative; margin: 2px 2px 8px; }
@@ -4197,20 +4209,25 @@ def _drawers_html() -> str:
4197
  # Library view (home): Mine/Community filter + a "+ Create a kata" card on
4198
  # top of the list, then the saved-kata cards.
4199
  '<div id="kimodo-cz-library" class="kimodo-cz-view cz-active">',
 
 
 
4200
  '<div class="kimodo-cz-seg kimodo-cz-seg--sub" id="kimodo-cz-scope">',
4201
  '<button type="button" class="kimodo-cz-segbtn active" data-scope="mine">Mine</button>',
4202
  '<button type="button" class="kimodo-cz-segbtn" data-scope="community">Community</button>',
4203
  '</div>',
4204
- # Browse controls: search filter + sort (Top by votes / Newest / Featured).
4205
- # Shown only on the Community tab (toggled in renderComposeLibrary).
 
4206
  '<div class="kimodo-cz-libctrls" id="kimodo-cz-libctrls">',
4207
- '<input id="kimodo-cz-cfilter" class="kimodo-cz-pfilter kimodo-cz-cfilter" type="text" placeholder="Search katas…" autocomplete="off" />',
4208
  '<select id="kimodo-cz-csort" class="kimodo-cz-csort" title="Sort / filter">',
4209
  '<option value="top">▲ Top</option>',
4210
  '<option value="newest">Newest</option>',
4211
  '<option value="featured">★ Featured</option>',
4212
  '</select>',
4213
  '</div>',
 
4214
  '<div id="kimodo-cz-katalist" class="kimodo-cz-katalist"></div>',
4215
  '</div>',
4216
  # Kata view (unified): tap a kata → view/edit it here with a scrubbable
@@ -5397,8 +5414,9 @@ _COMPOSE_JS = r"""
5397
  function renderComposeLibrary() {
5398
  const list = document.getElementById('kimodo-cz-katalist'); if (!list) return;
5399
  document.querySelectorAll('#kimodo-cz-scope .kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === composeScope));
5400
- // Browse controls (search + sort/featured) are a Community-only concern.
5401
  const ctrls = document.getElementById('kimodo-cz-libctrls'); if (ctrls) ctrls.style.display = (composeScope === 'community') ? '' : 'none';
 
5402
  // Mine = the localStorage entries (recipe-backed, instant); Community = the
5403
  // server manifest. Mine cards are always recipe-editable; their filmstrips draw
5404
  // from stance ids, so they don't depend on per-clip record files.
@@ -5419,13 +5437,7 @@ _COMPOSE_JS = r"""
5419
  return (b.created || 0) - (a.created || 0);
5420
  });
5421
  list.innerHTML = '';
5422
- // + Create a kata only on Mine (you build into your own list, not the community feed).
5423
- if (composeScope === 'mine') {
5424
- const createCard = document.createElement('div'); createCard.className = 'kimodo-cz-createcard';
5425
- createCard.innerHTML = '<span style="font-size:16px">+</span><span>Create a kata</span>';
5426
- createCard.onclick = () => newComposeKata();
5427
- list.appendChild(createCard);
5428
- }
5429
  if (!shown.length) {
5430
  const e = document.createElement('div'); e.className = 'kimodo-cz-libempty';
5431
  e.textContent = composeScope === 'mine'
@@ -6589,6 +6601,8 @@ _COMPOSE_JS = r"""
6589
  if (cfilter) cfilter.addEventListener('input', () => { composeFilter = cfilter.value || ''; renderComposeLibrary(); });
6590
  const csort = document.getElementById('kimodo-cz-csort');
6591
  if (csort) { csort.value = composeSort; csort.addEventListener('change', () => { composeSort = csort.value || 'top'; renderComposeLibrary(); }); }
 
 
6592
  // Move-mode pill: only changes the mode while the kata is empty (then it's locked).
6593
  const modeSeg = document.getElementById('kimodo-cz-mode');
6594
  if (modeSeg) modeSeg.addEventListener('click', (e) => {
 
250
  .kimodo-cz-katafeat:hover:not(:disabled) { background: #7c6226 !important; color: #fff1c4 !important; }
251
  .kimodo-cz-katafeat:disabled { cursor: default; }
252
  .kimodo-cz-katafeat.featured { background: #8a6d12 !important; border-color: #d9ad33 !important; color: #fff6d6 !important; cursor: default; }
253
+ /* SHARED browse header (kata + dojo libraries): the Mine/Community pill + create/add
254
+ button + search/sort, anchored (sticky) at the top of the scrolling list so they don't
255
+ scroll away. Both libraries are their own scroll container, so `position:sticky` works. */
256
+ .kimodo-libhead { position: sticky; top: 0; z-index: 8; display: flex; flex-direction: column; gap: 8px; padding: 2px 0 9px; margin-bottom: 4px; background: #16161b; border-bottom: 1px solid #2a2233; }
257
+ .kimodo-libhead .kimodo-cz-seg, .kimodo-libhead .kimodo-cz-libctrls, .kimodo-libhead .kimodo-cz-createbtn, .kimodo-libhead .kimodo-dojo-addbtn { margin: 0 !important; }
258
+ /* Shared search input (kata + dojo). */
259
+ .kimodo-libsearch { width: 100%; box-sizing: border-box; padding: 8px 10px; font-size: 13px; color: #f2e6ff; background: #1c1622; border: 1px solid #4a3a60; border-radius: 8px; }
260
+ .kimodo-libsearch::placeholder { color: #8a7aa0; }
261
+ .kimodo-libsearch:focus { outline: none; border-color: #a86fe0; }
262
  /* Library browse controls: search filter + sort select. */
263
+ .kimodo-cz-libctrls { display: flex; gap: 6px; }
264
  .kimodo-cz-cfilter { flex: 1; margin-bottom: 0 !important; }
265
  .kimodo-cz-csort { flex: 0 0 auto; padding: 8px 8px; font-size: 12px; color: #f2e6ff; background: #1c1622; border: 1px solid #4a3a60; border-radius: 8px; cursor: pointer; }
266
  .kimodo-cz-csort:focus { outline: none; border-color: #a86fe0; }
267
+ /* Header "Create a kata" button (was an in-list card; now anchored, Mine only). */
268
+ .kimodo-cz-createbtn { display: flex; align-items: center; justify-content: center; gap: 7px; width: 100%; padding: 11px; border-radius: 9px; cursor: pointer; font-size: 13px; font-weight: 600; color: #e6c7ff !important; background: #3a2a55 !important; border: 1px dashed #a86fe0 !important; box-shadow: none !important; }
269
+ .kimodo-cz-createbtn:hover { background: #46356a !important; }
270
  /* Card footer: upvote on the left, "★ Featured" badge pushed to the lower-right. */
271
  .kimodo-cz-cardfoot { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
272
  /* Per-card upvote: ▲ + count; filled when this browser has voted. */
 
749
  .kimodo-cz-tlcard.end .kimodo-cz-tlnode.endn { border-color: #d9776a; }
750
  .kimodo-cz-tlcard.gen { background: #1d1730; border-color: #4a3a6a; } /* saved move = darker purple */
751
  .kimodo-cz-tlcard.cz-pending { background: #232026; border-color: #4a474f; }
752
+ .kimodo-cz-tlcard.cz-active { border-color: #7dd3fc; box-shadow: 0 0 0 1px #7dd3fc; }
753
+ .kimodo-cz-tlcard.sel { box-shadow: 0 0 0 2px #7dd3fc; }
754
  .kimodo-cz-tlcard.cz-justgen { animation: kimodo-cz-fromgray-card .55s ease; }
755
  .kimodo-cz-pcardrow.cz-pop .kimodo-cz-tlcard { animation: kimodo-cz-pop .5s cubic-bezier(.34,1.56,.64,1) both; }
756
  .kimodo-cz-tlmeta { flex: 1; min-width: 0; }
 
761
  .kimodo-cz-tlx { position: absolute; top: 3px; right: 5px; }
762
  .kimodo-cz-tlgrip { position: absolute; left: 0; right: 0; bottom: 0; height: 15px; display: flex; align-items: center; justify-content: center; cursor: ns-resize; touch-action: none; z-index: 5; }
763
  .kimodo-cz-tlgrip span { width: 28px; height: 3px; border-radius: 2px; background: #6a5a86; transition: background .15s ease; }
764
+ .kimodo-cz-tlgrip:hover span, .kimodo-cz-tlcard.cz-active .kimodo-cz-tlgrip span { background: #7dd3fc; }
765
  /* active-move controls below the timeline */
766
  .kimodo-cz-controls { margin-top: 12px; padding-top: 10px; border-top: 1px solid #2a2233; }
767
  .kimodo-cz-controlslab { font-size: 11px; font-weight: 600; color: #c9b6ea; margin-bottom: 6px; }
 
769
  to set the length = the gap between them), GENERATE right under the end, then the
770
  collapsed refine prompt + the move/rotation numbers. ---- */
771
  .kimodo-cz-activerow { overflow: visible; }
772
+ .kimodo-cz-editcard { position: relative; border: 1px solid #7dd3fc; border-radius: 10px; background: #241a30; padding: 8px; box-shadow: 0 0 0 1px rgba(125,211,252,0.35); }
773
  /* A move edited after it was generated → dirty (amber) so it's clearly "needs Refresh". */
774
  .kimodo-cz-editcard.cz-dirty { border-color: #5a5a64; background: #26262c; box-shadow: 0 0 0 1px rgba(90,90,100,0.35); } /* unsaved edit = gray (like a new move) */
775
  /* Not generated yet → gray. Once generated it pulses gray→purple; selecting a generated
 
778
  .kimodo-cz-editcard.cz-genpulse { animation: kimodo-cz-gray2purple .6s ease both; }
779
  .kimodo-cz-editcard.cz-selpulse { animation: kimodo-cz-green2purple .6s ease both; }
780
  @keyframes kimodo-cz-gray2purple {
781
+ 0% { background: #26262c; border-color: #5a5a64; box-shadow: 0 0 0 0 rgba(125,211,252,0); }
782
+ 45% { box-shadow: 0 0 0 5px rgba(125,211,252,0.5); transform: scale(1.02); }
783
+ 100% { background: #241a30; border-color: #7dd3fc; box-shadow: 0 0 0 1px rgba(125,211,252,0.35); transform: scale(1); }
784
  }
785
  @keyframes kimodo-cz-green2purple {
786
+ 0% { background: #1d1730; border-color: #4a3a6a; box-shadow: 0 0 0 0 rgba(125,211,252,0); } /* from the darker-purple saved card */
787
+ 45% { box-shadow: 0 0 0 5px rgba(125,211,252,0.5); transform: scale(1.02); }
788
+ 100% { background: #241a30; border-color: #7dd3fc; box-shadow: 0 0 0 1px rgba(125,211,252,0.35); transform: scale(1); }
789
  }
790
  .kimodo-cz-emmovelab { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: #c9b6ea; margin: 0 0 5px 2px; }
791
  .kimodo-cz-emregion { position: relative; margin: 2px 2px 8px; }
 
4209
  # Library view (home): Mine/Community filter + a "+ Create a kata" card on
4210
  # top of the list, then the saved-kata cards.
4211
  '<div id="kimodo-cz-library" class="kimodo-cz-view cz-active">',
4212
+ # Anchored (sticky) browse header: pill + Create button (Mine) + search/sort
4213
+ # (Community). renderComposeLibrary toggles which controls show per scope.
4214
+ '<div class="kimodo-libhead">',
4215
  '<div class="kimodo-cz-seg kimodo-cz-seg--sub" id="kimodo-cz-scope">',
4216
  '<button type="button" class="kimodo-cz-segbtn active" data-scope="mine">Mine</button>',
4217
  '<button type="button" class="kimodo-cz-segbtn" data-scope="community">Community</button>',
4218
  '</div>',
4219
+ # Create a kata Mine only.
4220
+ '<button type="button" id="kimodo-cz-create" class="kimodo-cz-createbtn"><span style="font-size:16px">+</span> Create a kata</button>',
4221
+ # Search + sort (Top by votes / Newest / Featured) — Community only.
4222
  '<div class="kimodo-cz-libctrls" id="kimodo-cz-libctrls">',
4223
+ '<input id="kimodo-cz-cfilter" class="kimodo-libsearch kimodo-cz-cfilter" type="text" placeholder="🔍 search" autocomplete="off" />',
4224
  '<select id="kimodo-cz-csort" class="kimodo-cz-csort" title="Sort / filter">',
4225
  '<option value="top">▲ Top</option>',
4226
  '<option value="newest">Newest</option>',
4227
  '<option value="featured">★ Featured</option>',
4228
  '</select>',
4229
  '</div>',
4230
+ '</div>', # /libhead
4231
  '<div id="kimodo-cz-katalist" class="kimodo-cz-katalist"></div>',
4232
  '</div>',
4233
  # Kata view (unified): tap a kata → view/edit it here with a scrubbable
 
5414
  function renderComposeLibrary() {
5415
  const list = document.getElementById('kimodo-cz-katalist'); if (!list) return;
5416
  document.querySelectorAll('#kimodo-cz-scope .kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === composeScope));
5417
+ // Anchored header: Create button is Mine-only; search + sort/featured are Community-only.
5418
  const ctrls = document.getElementById('kimodo-cz-libctrls'); if (ctrls) ctrls.style.display = (composeScope === 'community') ? '' : 'none';
5419
+ const createBtn = document.getElementById('kimodo-cz-create'); if (createBtn) createBtn.style.display = (composeScope === 'mine') ? '' : 'none';
5420
  // Mine = the localStorage entries (recipe-backed, instant); Community = the
5421
  // server manifest. Mine cards are always recipe-editable; their filmstrips draw
5422
  // from stance ids, so they don't depend on per-clip record files.
 
5437
  return (b.created || 0) - (a.created || 0);
5438
  });
5439
  list.innerHTML = '';
5440
+ // (The "+ Create a kata" button now lives in the anchored header, Mine only.)
 
 
 
 
 
 
5441
  if (!shown.length) {
5442
  const e = document.createElement('div'); e.className = 'kimodo-cz-libempty';
5443
  e.textContent = composeScope === 'mine'
 
6601
  if (cfilter) cfilter.addEventListener('input', () => { composeFilter = cfilter.value || ''; renderComposeLibrary(); });
6602
  const csort = document.getElementById('kimodo-cz-csort');
6603
  if (csort) { csort.value = composeSort; csort.addEventListener('change', () => { composeSort = csort.value || 'top'; renderComposeLibrary(); }); }
6604
+ const ccreate = document.getElementById('kimodo-cz-create');
6605
+ if (ccreate) ccreate.addEventListener('click', () => newComposeKata());
6606
  // Move-mode pill: only changes the mode while the kata is empty (then it's locked).
6607
  const modeSeg = document.getElementById('kimodo-cz-mode');
6608
  if (modeSeg) modeSeg.addEventListener('click', (e) => {
tabs/dojo.drawer.html CHANGED
@@ -2,11 +2,16 @@
2
 
3
  <!-- ============ LIBRARY VIEW: list of dojos (Mine / Community) ============ -->
4
  <div id="kimodo-dojo-library" class="kimodo-dojo-view">
5
- <div class="kimodo-cz-seg kimodo-cz-seg--sub kimodo-dojo-seg" id="kimodo-dojo-scope" aria-label="Dojo scope">
6
- <button type="button" class="kimodo-cz-segbtn active" data-scope="mine">Mine</button>
7
- <button type="button" class="kimodo-cz-segbtn" data-scope="community">Community</button>
 
 
 
 
 
 
8
  </div>
9
- <button id="kimodo-dojo-add" class="kimodo-dojo-addbtn" type="button">+ Add dojo</button>
10
  <div id="kimodo-dojo-grid" class="kimodo-dojo-cards" aria-label="Dojo list"></div>
11
  <div id="kimodo-dojo-empty" class="kimodo-drawer-sub kimodo-dojo-empty"></div>
12
  </div>
 
2
 
3
  <!-- ============ LIBRARY VIEW: list of dojos (Mine / Community) ============ -->
4
  <div id="kimodo-dojo-library" class="kimodo-dojo-view">
5
+ <!-- Anchored (sticky) browse header — same pattern as the kata library: pill + Add
6
+ (Mine only) + search (Community only). Toggled per scope in renderLibrary. -->
7
+ <div class="kimodo-libhead">
8
+ <div class="kimodo-cz-seg kimodo-cz-seg--sub kimodo-dojo-seg" id="kimodo-dojo-scope" aria-label="Dojo scope">
9
+ <button type="button" class="kimodo-cz-segbtn active" data-scope="mine">Mine</button>
10
+ <button type="button" class="kimodo-cz-segbtn" data-scope="community">Community</button>
11
+ </div>
12
+ <button id="kimodo-dojo-add" class="kimodo-dojo-addbtn" type="button">+ Add dojo</button>
13
+ <input id="kimodo-dojo-search" class="kimodo-libsearch" type="text" placeholder="🔍 search" autocomplete="off" />
14
  </div>
 
15
  <div id="kimodo-dojo-grid" class="kimodo-dojo-cards" aria-label="Dojo list"></div>
16
  <div id="kimodo-dojo-empty" class="kimodo-drawer-sub kimodo-dojo-empty"></div>
17
  </div>
tabs/dojo.tab.js CHANGED
@@ -3,6 +3,7 @@
3
  let dojoMode = 'splat';
4
  let lastScene = null;
5
  let dojoScope = 'mine';
 
6
  let improvedByAya = false; // current prompt came from tiny-aya → button reads "reroll"
7
  let improving = false; // a tiny-aya suggestion call is in flight
8
  let _communityCache = null; // canonical + contributed dojos fetched from the dataset
@@ -25,6 +26,7 @@
25
  create: document.getElementById('kimodo-dojo-create'),
26
  scope: document.getElementById('kimodo-dojo-scope'),
27
  add: document.getElementById('kimodo-dojo-add'),
 
28
  back: document.getElementById('kimodo-dojo-back'),
29
  grid: document.getElementById('kimodo-dojo-grid'),
30
  emptyMsg: document.getElementById('kimodo-dojo-empty'),
@@ -449,14 +451,22 @@
449
  return card;
450
  }
451
 
 
 
 
 
 
 
452
  function renderLibrary() {
453
  const els = dojoEls();
454
  if (els.scope) els.scope.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === dojoScope));
 
 
455
  if (!els.grid) return;
456
  els.grid.innerHTML = '';
457
  if (dojoScope === 'mine') {
458
  els.grid.appendChild(clearCard()); // first card: clear + back to the grid floor
459
- const arr = loadMyDojos();
460
  arr.forEach((e) => els.grid.appendChild(mineCard(e)));
461
  if (els.emptyMsg) els.emptyMsg.textContent = arr.length ? '' : 'No dojos yet — tap + Add dojo to create one.';
462
  return;
@@ -467,9 +477,10 @@
467
  if (dojoScope !== 'community' || !els.grid) return;
468
  els.grid.innerHTML = '';
469
  els.grid.appendChild(clearCard());
470
- if (list && list.length) list.forEach((d) => els.grid.appendChild(communityRealCard(d)));
471
- else COMMUNITY_SEED.forEach((s) => els.grid.appendChild(communityCard(s)));
472
- if (els.emptyMsg) els.emptyMsg.textContent = '';
 
473
  });
474
  }
475
 
@@ -609,6 +620,7 @@
609
  renderLibrary();
610
  });
611
  if (els.add) els.add.addEventListener('click', () => openCreate(null));
 
612
  if (els.back) els.back.addEventListener('click', () => showView('library'));
613
  if (els.improve) els.improve.addEventListener('click', () => { if (!busy && !improving) improvePrompt(); });
614
  if (els.save) els.save.addEventListener('click', () => { if (!els.save.disabled) saveDojoToDataset(); });
 
3
  let dojoMode = 'splat';
4
  let lastScene = null;
5
  let dojoScope = 'mine';
6
+ let dojoSearch = ''; // library search query (matches dojo name)
7
  let improvedByAya = false; // current prompt came from tiny-aya → button reads "reroll"
8
  let improving = false; // a tiny-aya suggestion call is in flight
9
  let _communityCache = null; // canonical + contributed dojos fetched from the dataset
 
26
  create: document.getElementById('kimodo-dojo-create'),
27
  scope: document.getElementById('kimodo-dojo-scope'),
28
  add: document.getElementById('kimodo-dojo-add'),
29
+ search: document.getElementById('kimodo-dojo-search'),
30
  back: document.getElementById('kimodo-dojo-back'),
31
  grid: document.getElementById('kimodo-dojo-grid'),
32
  emptyMsg: document.getElementById('kimodo-dojo-empty'),
 
451
  return card;
452
  }
453
 
454
+ // Anchored header (mirrors the kata library): Add button is Mine-only, search is
455
+ // Community-only. Match a dojo by name against the search query.
456
+ function dojoMatches(name) {
457
+ const q = (dojoSearch || '').trim().toLowerCase();
458
+ return !q || String(name || '').toLowerCase().includes(q);
459
+ }
460
  function renderLibrary() {
461
  const els = dojoEls();
462
  if (els.scope) els.scope.querySelectorAll('.kimodo-cz-segbtn').forEach((b) => b.classList.toggle('active', b.dataset.scope === dojoScope));
463
+ if (els.add) els.add.style.display = (dojoScope === 'mine') ? '' : 'none';
464
+ if (els.search) els.search.style.display = (dojoScope === 'community') ? '' : 'none';
465
  if (!els.grid) return;
466
  els.grid.innerHTML = '';
467
  if (dojoScope === 'mine') {
468
  els.grid.appendChild(clearCard()); // first card: clear + back to the grid floor
469
+ const arr = loadMyDojos().filter((e) => dojoMatches(e.name));
470
  arr.forEach((e) => els.grid.appendChild(mineCard(e)));
471
  if (els.emptyMsg) els.emptyMsg.textContent = arr.length ? '' : 'No dojos yet — tap + Add dojo to create one.';
472
  return;
 
477
  if (dojoScope !== 'community' || !els.grid) return;
478
  els.grid.innerHTML = '';
479
  els.grid.appendChild(clearCard());
480
+ const real = (list || []).filter((d) => dojoMatches(d.name));
481
+ if (list && list.length) real.forEach((d) => els.grid.appendChild(communityRealCard(d)));
482
+ else COMMUNITY_SEED.filter((s) => dojoMatches(s.name)).forEach((s) => els.grid.appendChild(communityCard(s)));
483
+ if (els.emptyMsg) els.emptyMsg.textContent = (list && list.length && !real.length) ? 'No dojos match.' : '';
484
  });
485
  }
486
 
 
620
  renderLibrary();
621
  });
622
  if (els.add) els.add.addEventListener('click', () => openCreate(null));
623
+ if (els.search) els.search.addEventListener('input', () => { dojoSearch = els.search.value || ''; renderLibrary(); });
624
  if (els.back) els.back.addEventListener('click', () => showView('library'));
625
  if (els.improve) els.improve.addEventListener('click', () => { if (!busy && !improving) improvePrompt(); });
626
  if (els.save) els.save.addEventListener('click', () => { if (!els.save.disabled) saveDojoToDataset(); });