studio-engine 0.27.0 → 0.28.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: 3fefcb19f5cf631e4e1515bf0b113fb33167cb1173f039a0777038aac5173d57
4
- data.tar.gz: f203c29195b71e945a055bcfa60ad58f0d1aea16fe2ce60e84de4de1993d62ec
3
+ metadata.gz: 79aac649d7403deadeaa9f055762802f111b7082865398432753dfdd93b998e0
4
+ data.tar.gz: 80307a1b7077886c9853ad5a6ab7f2dc79a9d7bf6d4db7aa883f5eb6162a77bb
5
5
  SHA512:
6
- metadata.gz: d790f97aede359baf45915bbde14fef29cea6779a0866159a3b5f1dc5a396d24951d7ba3d796e4523e0dc17489319375929e8b3e2741ca9b7c569dd5f9dac91e
7
- data.tar.gz: 25a54c2e44ad2e29608afe72fcf6b7dc8028fe28c47bd92e582e041b282f7f4e2b70ba5eb3468f4d8edb083f3d1fe185c340c135678b9a0a0556bfbab601a98b
6
+ metadata.gz: 8fb8f96918656e88d2d8cc3379716d544afd461bdaa19205c480e273ab71199c25cede8c7b729f9b6cf3400388a7976ac34f4135d96fd60b3e8e4083969f360e
7
+ data.tar.gz: 2f329ab8c0c240cf56e11e2e9d7bc1c3c6711e24dcb52ee82a2de2785c1697cf333e3bcc17f056db22400e4e606d6c30108565502bd8a6119e2d7b50e18a1a64
@@ -48,9 +48,16 @@
48
48
  onMoveHook: opts.onMoveHook || null,
49
49
  onDropHook: opts.onDropHook || null,
50
50
  labels: opts.labels || {},
51
+ // gap 3: the column key a data-exit-action="archive" remove re-parents INTO
52
+ // (its #dropzone-<key>) rather than deleting the card. null ⇒ 0.27.0 removal.
53
+ archiveZone: opts.archiveZone || null,
51
54
 
52
55
  // --- state ---------------------------------------------------------
53
56
  toasts: [],
57
+ // gap 1: board-level chrome state lives HERE, on the studioBoard scope, so a
58
+ // board's header_slot controls + column show_expr bind to it directly — no
59
+ // bespoke outer Alpine wrapper. Seeded from board_state; {} when omitted.
60
+ state: (opts.state && typeof opts.state === "object") ? opts.state : {},
54
61
 
55
62
  // --- init ----------------------------------------------------------
56
63
  init: function () {
@@ -75,6 +82,19 @@
75
82
  zoneKey: function (el) { return el ? el.getAttribute("data-" + this.zoneAttr) : null; },
76
83
  label: function (zone) { return this.labels[zone] || zone; },
77
84
 
85
+ // --- board-chrome state helpers (gap 1) ----------------------------
86
+ // Generic, domain-free operations over `state` so a board's header_slot /
87
+ // column show_expr drive filters + toggles without a bespoke wrapper.
88
+ toggleState: function (key) { this.state[key] = !this.state[key]; },
89
+ listHas: function (key, val) {
90
+ return Array.isArray(this.state[key]) && this.state[key].indexOf(val) !== -1;
91
+ },
92
+ toggleInState: function (key, val) {
93
+ if (!Array.isArray(this.state[key])) this.state[key] = [];
94
+ var i = this.state[key].indexOf(val);
95
+ if (i === -1) { this.state[key].push(val); } else { this.state[key].splice(i, 1); }
96
+ },
97
+
78
98
  // --- SortableJS wiring ---------------------------------------------
79
99
  initSortables: function () {
80
100
  if (typeof Sortable === "undefined") return;
@@ -271,6 +291,21 @@
271
291
  var card = document.getElementById(target);
272
292
  if (!card) { renderNow(el); return; }
273
293
  self.animateCardExit(card, exitAction).then(function () {
294
+ // gap 3: an `archive` exit RE-PARENTS the card into the archive
295
+ // column (#dropzone-<archiveZone>) instead of vanishing, when the
296
+ // board named one and it exists. Everything else falls through to
297
+ // Turbo's remove (the 0.27.0 behavior).
298
+ if (exitAction === "archive" && self.archiveZone) {
299
+ var zone = document.getElementById("dropzone-" + self.archiveZone);
300
+ if (zone) {
301
+ var anchor = self.emptySelector ? zone.querySelector(self.emptySelector) : null;
302
+ zone.insertBefore(card, anchor);
303
+ self.setCardZone(card, self.archiveZone);
304
+ self.resetCardExit(card);
305
+ self.updateCounts();
306
+ return; // keep the card in the archive column — do NOT remove it
307
+ }
308
+ }
274
309
  renderNow(el);
275
310
  self.updateCounts();
276
311
  });
@@ -279,7 +314,13 @@
279
314
  },
280
315
 
281
316
  animateCardExit: function (card, kind) {
282
- if (!card || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return Promise.resolve();
317
+ if (!card) return Promise.resolve();
318
+ // gap 4: stamp an observable marker for WHICH exit is running (archive vs
319
+ // delete). Set before the reduced-motion guard so the marker is present
320
+ // even when the animation itself is skipped — the re-parent branch and any
321
+ // e2e both key off it.
322
+ card.dataset.exitAction = kind;
323
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return Promise.resolve();
283
324
  card.style.pointerEvents = "none";
284
325
  var frames = kind === "archive"
285
326
  ? [{ opacity: 1, transform: "translateY(0) scale(1)" }, { opacity: 0, transform: "translateY(18px) scale(0.86)" }]
@@ -292,6 +333,18 @@
292
333
  return anim.finished.catch(function () {});
293
334
  },
294
335
 
336
+ // Clear an exit animation + its marker so a re-parented card is fully
337
+ // visible again in the archive column (used by the gap-3 re-parent path).
338
+ resetCardExit: function (card) {
339
+ if (!card) return;
340
+ card.getAnimations({ subtree: false }).forEach(function (a) { a.cancel(); });
341
+ delete card.dataset.exitAction;
342
+ card.style.pointerEvents = "";
343
+ card.style.opacity = "";
344
+ card.style.transform = "";
345
+ card.style.filter = "";
346
+ },
347
+
295
348
  // --- toasts --------------------------------------------------------
296
349
  toast: function (message, type) {
297
350
  if (!this.toastsEnabled) {
@@ -19,10 +19,27 @@
19
19
  studio/board/_column (the `.kanban-dropzone`) and studio/board/_card_shell (the
20
20
  `.kanban-card`) — an app may hand-roll a bespoke card as long as it matches.
21
21
 
22
+ CHROME COMPOSITION (the recipe that replaces a bespoke outer Alpine wrapper) —
23
+ a board with filters / toggles no longer needs to wrap this primitive in its own
24
+ x-data component. Everything the chrome needs lives ON the studioBoard scope:
25
+ 1. `board_state:` seeds a reactive bag on the factory (`state`). Pass the
26
+ chrome's flags/lists, e.g. { showArchived: false, hiddenApps: [] }.
27
+ 2. `header_slot:` (and `above_board_slot:`) render INSIDE the studioBoard
28
+ section, so their controls bind straight to that state and the generic
29
+ helpers the factory exposes: `toggleState('showArchived')`,
30
+ `toggleInState('hiddenApps', repo)`, `listHas('hiddenApps', repo)`.
31
+ 3. A toggle-able column reads the same state via its `show_expr:`
32
+ (e.g. show_expr: "state.showArchived"); cards x-show off `listHas(...)`.
33
+ 4. An Archived column that a stage→archived broadcast should re-parent INTO
34
+ (not delete) is named by the board's `archive_zone:`.
35
+
22
36
  locals:
23
37
  columns: (req) [{ key:, label:, cards:, count:, badge_class:,
24
- collapsible:, hidden:, header_extra: }]. `cards` are records
25
- passed to `card_partial`; `count` defaults to cards.size.
38
+ count_class:, collapsible:, hidden:, show_expr:, min_width:,
39
+ kickoff:, header_extra: }]. `cards` are records passed to
40
+ `card_partial`; `count` defaults to cards.size. See
41
+ studio/board/_column for the additive per-column hooks
42
+ (count_class / show_expr / min_width / kickoff).
26
43
  card_partial: (req) partial rendered per card (e.g. "tasks/task_card").
27
44
  card_as: local the record binds to in card_partial. Default :card.
28
45
  card_locals: per-card extra locals — a Proc ->(record){ Hash } or a Hash.
@@ -50,6 +67,11 @@
50
67
  empty_label: empty-zone text. Default "Drop here".
51
68
  header_slot: raw HTML rendered above the columns (pass html_safe content).
52
69
  above_board_slot: raw HTML rendered between the header and the columns.
70
+ board_state: Hash seeding the factory's reactive `state` bag (board-level
71
+ chrome: filter/toggle flags + lists). nil ⇒ empty {}.
72
+ archive_zone: column key a `remove` exit-stream marked data-exit-action=
73
+ "archive" RE-PARENTS the card into (its #dropzone-<key>),
74
+ instead of deleting it. nil ⇒ the card is removed (0.27.0).
53
75
  fx: window[<name>] module the factory defers add/exit anims to.
54
76
  on_move_hook: window[<name>] opaque fn, called with { record, from, to }.
55
77
  on_drop_hook: window[<name>] opaque fn, called with { ids, zone }.
@@ -79,6 +101,8 @@
79
101
  empty_label = local_assigns.fetch(:empty_label, "Drop here")
80
102
  header_slot = local_assigns.fetch(:header_slot, nil)
81
103
  above_board_slot = local_assigns.fetch(:above_board_slot, nil)
104
+ board_state = local_assigns.fetch(:board_state, nil) # gap 1: chrome-state home
105
+ archive_zone = local_assigns.fetch(:archive_zone, nil) # gap 3: exit re-parent target
82
106
  fx = local_assigns.fetch(:fx, nil)
83
107
  on_move_hook = local_assigns.fetch(:on_move_hook, nil)
84
108
  on_drop_hook = local_assigns.fetch(:on_drop_hook, nil)
@@ -102,6 +126,8 @@
102
126
  toasts: !!toasts,
103
127
  live: live_channel.present?,
104
128
  demo: !!demo,
129
+ state: (board_state.is_a?(Hash) ? board_state : nil),
130
+ archiveZone: (archive_zone ? archive_zone.to_s : nil),
105
131
  fx: fx,
106
132
  onMoveHook: on_move_hook,
107
133
  onDropHook: on_drop_hook,
@@ -12,10 +12,25 @@
12
12
 
13
13
  locals:
14
14
  column: (req) { key:, label:, cards:, count:, badge_class:,
15
- collapsible:, hidden:, header_extra: }. `cards` are the
15
+ count_class:, collapsible:, hidden:, show_expr:,
16
+ min_width:, kickoff:, header_extra: }. `cards` are the
16
17
  records passed to the card partial; `count` defaults to
17
18
  cards.size; `badge_class` picks a components/_badge scheme
18
19
  for the count (else a plain neutral count chip).
20
+ Additive board-chrome hooks (all opt-in, all default to
21
+ the 0.27.0 rendering when omitted):
22
+ count_class: raw class string colouring the neutral
23
+ count chip (e.g. a task_stage_count_classes value).
24
+ Ignored when badge_class is set. Default neutral chip.
25
+ show_expr: Alpine x-show expression for a toggle-able
26
+ column (binds against the board's studioBoard scope,
27
+ e.g. "state.showArchived"). Adds x-cloak so it can't
28
+ flash before Alpine boots. Independent of `hidden:`.
29
+ min_width: the column's sm min-width utility class.
30
+ Default "sm:min-w-[190px]".
31
+ kickoff: raw HTML rendered in a fixed-height slot
32
+ under the header row (a per-column kickoff chip);
33
+ nil renders nothing.
19
34
  zone_attr: data-attr name for the zone. Default "stage".
20
35
  dom_id_prefix: card dom-id prefix, forwarded to the cards. Default "card-".
21
36
  zone_selector_class: the drop-zone class. Default "kanban-dropzone".
@@ -41,9 +56,20 @@
41
56
  cards = column[:cards] || []
42
57
  count = column.fetch(:count) { cards.size }
43
58
  badge_class = column[:badge_class]
59
+ # gap 2: an opt-in raw class colouring the neutral count chip; nil keeps the
60
+ # exact 0.27.0 neutral chip classes.
61
+ count_class = column[:count_class]
44
62
  collapsible = column[:collapsible]
45
63
  hidden = column[:hidden]
64
+ # gap 5: a bindable visibility expr (x-show), a per-board min-width, and a
65
+ # per-column kickoff slot — each nil/absent renders identically to 0.27.0.
66
+ show_expr = column[:show_expr]
67
+ min_width = column[:min_width] || "sm:min-w-[190px]"
68
+ kickoff = column[:kickoff]
46
69
  header_extra = column[:header_extra]
70
+ # The neutral count chip's colour classes: an opt-in raw class, else the 0.27.0
71
+ # neutral default. The layout classes are shared; only the colour band varies.
72
+ count_color = count_class || "bg-surface-alt text-secondary border border-subtle"
47
73
 
48
74
  # Merge the per-card hook (Proc or Hash) with the record binding.
49
75
  resolve_locals = lambda do |record|
@@ -51,21 +77,29 @@
51
77
  { card_as => record }.merge(extra || {})
52
78
  end
53
79
  %>
54
- <div class="studio-board-column w-full flex-none sm:flex-1 sm:min-w-[190px]"
80
+ <div class="studio-board-column w-full flex-none sm:flex-1 <%= min_width %>"
55
81
  data-board-column="<%= key %>"
56
82
  <% if collapsible %>data-board-collapsible="true"<% end %>
83
+ <% if show_expr %>x-show="<%= show_expr %>" x-cloak<% end %>
57
84
  <% if hidden %>x-cloak style="display:none"<% end %>>
58
85
  <div class="mb-3 px-1 flex items-center gap-2">
59
86
  <h3 class="text-sm font-bold text-heading uppercase tracking-wider"><%= label %></h3>
60
87
  <% if badge_class %>
61
88
  <%= render "components/badge", text: count.to_s, scheme: badge_class, data_board_count: key %>
62
89
  <% else %>
63
- <span class="stage-count inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-bold bg-surface-alt text-secondary border border-subtle"
90
+ <span class="stage-count inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-full text-xs font-bold <%= count_color %>"
64
91
  data-board-count="<%= key %>"><%= count %></span>
65
92
  <% end %>
66
93
  <% if header_extra %><%= header_extra %><% end %>
67
94
  </div>
68
95
 
96
+ <%# gap 5: per-column kickoff slot — a fixed-height row under the header so every
97
+ column's dropzone starts at the same y (no height jog) when only some carry a
98
+ chip. Renders nothing when the column passes no kickoff. %>
99
+ <% if kickoff %>
100
+ <div class="studio-board-kickoff mt-2 min-h-[26px]"><%= kickoff %></div>
101
+ <% end %>
102
+
69
103
  <div id="dropzone-<%= key %>"
70
104
  data-<%= zone_attr %>="<%= key %>"
71
105
  class="<%= zone_selector_class %> rounded-xl border-2 border-dashed border-subtle min-h-40 p-2 space-y-2 transition-colors duration-150 sm:min-h-[calc(100vh-220px)]">
@@ -109,6 +109,69 @@
109
109
  </div>
110
110
  </div>
111
111
 
112
+ <%# ---- The ENRICHED board chrome (0.28.0) ------------------------------ %>
113
+ <%# One specimen that exercises the five additive hooks the /tasks pilot surfaced,
114
+ all opt-in: (1) board_state + header_slot chrome home, (2) a coloured count
115
+ chip via count_class, (3) archive_zone exit re-parent, (4) the exit marker,
116
+ (5) a toggle-able column (show_expr) + a per-column kickoff slot + min_width.
117
+ No outer Alpine wrapper: the Show/Hide-archived toggle in header_slot and the
118
+ archived column's show_expr both bind to the SAME studioBoard state. %>
119
+ <% chrome_header = capture do %>
120
+ <div class="flex items-center gap-3 flex-wrap">
121
+ <button type="button"
122
+ class="px-3 py-1.5 rounded-lg text-sm font-medium transition border border-subtle"
123
+ :class="state.showArchived ? 'bg-primary text-white border-primary' : 'bg-surface text-secondary hover:text-heading'"
124
+ @click="toggleState('showArchived')"
125
+ data-test="chrome-archived-toggle">
126
+ <span x-text="state.showArchived ? 'Hide archived' : 'Show archived'">Show archived</span>
127
+ </button>
128
+ <span class="label-upper"><code class="font-mono text-2xs">header_slot</code> chrome &middot; binds to <code class="font-mono text-2xs">state.showArchived</code> &mdash; no outer wrapper</span>
129
+ </div>
130
+ <% end %>
131
+ <% chrome_kickoff = capture do %>
132
+ <span class="inline-flex items-center gap-1 rounded border border-primary/30 bg-primary/10 px-1.5 py-0.5 text-2xs font-semibold uppercase tracking-wide text-primary">kickoff slot</span>
133
+ <% end %>
134
+ <%
135
+ # Per-column count_class colours (raw classes, the task_stage_count_classes shape),
136
+ # a kickoff chip + widened min_width on Designed, and a toggle-able Archived column
137
+ # (show_expr) the board re-parents into (archive_zone).
138
+ chrome_columns = [
139
+ { key: "designed", label: "Designed",
140
+ count_class: "bg-blue-500/10 text-blue-500 border border-blue-500/30",
141
+ kickoff: chrome_kickoff, min_width: "sm:min-w-[15rem]",
142
+ cards: [{ id: "chrome-count-color", zone: "designed", title: "Coloured count chip", repo: "studio-engine", who: "SH" }] },
143
+ { key: "building", label: "Building",
144
+ count_class: "bg-yellow-500/10 text-yellow-400 border border-yellow-500/30",
145
+ cards: [{ id: "chrome-bindable-col", zone: "building", title: "Bindable columns", repo: "turf-monster", who: "CA" }] },
146
+ { key: "shipped", label: "Shipped",
147
+ count_class: "bg-emerald-500/10 text-emerald-500 border border-emerald-500/30",
148
+ cards: [{ id: "chrome-exit-marker", zone: "shipped", title: "Exit re-parent marker", repo: "mcritchie", who: "ST" }] },
149
+ { key: "archived", label: "Archived",
150
+ count_class: "bg-gray-500/10 text-gray-400 border border-gray-500/30",
151
+ show_expr: "state.showArchived",
152
+ cards: [{ id: "chrome-archived-card", zone: "archived", title: "Archived (re-parent target)", repo: "mcritchie", who: "ST" }] }
153
+ ]
154
+ %>
155
+ <div class="space-y-3">
156
+ <div class="flex items-center gap-2">
157
+ <span class="badge" style="color: var(--color-primary); border-color: var(--color-primary)">Enriched</span>
158
+ <p class="label-upper"><code class="font-mono text-2xs">studio/board/board</code> &middot; 0.28.0 chrome &mdash; coloured counts, toggle-able column, archive re-parent</p>
159
+ </div>
160
+ <%= render "studio/board/board",
161
+ demo: true,
162
+ columns: chrome_columns,
163
+ card_partial: "style/board/demo_card",
164
+ card_as: :card,
165
+ reorder_url: "#",
166
+ move_url: "#",
167
+ move_param: { resource: "task", attr: "stage" },
168
+ group: "studio-board-chrome",
169
+ board_state: { showArchived: false },
170
+ archive_zone: "archived",
171
+ header_slot: chrome_header,
172
+ empty_label: "Drop tasks here" %>
173
+ </div>
174
+
112
175
  <%# ---- The static reference sketch ------------------------------------- %>
113
176
  <div class="fade-edge-x overflow-x-auto pb-2">
114
177
  <div class="flex gap-4 min-w-max">
@@ -162,9 +225,11 @@
162
225
  </div>
163
226
  <ul class="text-secondary text-sm space-y-1 list-disc list-inside">
164
227
  <li>Column layout + horizontal scroll with faded edges</li>
165
- <li>The <code class="font-mono text-2xs">stage-*</code> badge palette + count chips</li>
228
+ <li>The <code class="font-mono text-2xs">stage-*</code> badge palette + count chips (colour via <code class="font-mono text-2xs">count_class</code>)</li>
166
229
  <li>Card chrome (<code class="font-mono text-2xs">.card</code>) and the drag / rank handle</li>
167
230
  <li>Empty-column drop state</li>
231
+ <li>Chrome-state home (<code class="font-mono text-2xs">board_state</code> + <code class="font-mono text-2xs">header_slot</code>) &mdash; filters/toggles without an outer wrapper</li>
232
+ <li>Toggle-able columns (<code class="font-mono text-2xs">show_expr</code>), a kickoff slot, and archive re-parent (<code class="font-mono text-2xs">archive_zone</code>)</li>
168
233
  </ul>
169
234
  </div>
170
235
  <div class="card p-5 space-y-2">
@@ -175,7 +240,7 @@
175
240
  <ul class="text-secondary text-sm space-y-1 list-disc list-inside">
176
241
  <li>Which stages become columns</li>
177
242
  <li>The card body — fields, chips, avatars, live timers</li>
178
- <li>Filters (agent, app) and header actions</li>
243
+ <li>Filter/toggle logic + header actions, composed onto the shared chrome state</li>
179
244
  <li>Live-update wiring (Turbo Streams on the deploy board)</li>
180
245
  </ul>
181
246
  </div>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.27.0"
2
+ VERSION = "0.28.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.0
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie