studio-engine 0.22.0 → 0.24.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: 15b4c45fed9c5a2fd6056fca154236cbc12b778c483af5922486e47fcec6baac
4
- data.tar.gz: d1efa67d91b858e9ba81f9fea1cb63d4b73c1e76a97c08dd0a8ca2afcd0b4522
3
+ metadata.gz: 2aed8dd0e8d45e41602035462f0b60d01414ac3ea2f41484cbff9d4a40384cb8
4
+ data.tar.gz: e7aa1bfef279150c47cf3bd709de81b5b174f9027252a177a58a19e4609cf85c
5
5
  SHA512:
6
- metadata.gz: ec7d708e3665fbccf82ee641f82110cc8adfda8dd19528b3748807697ae6127db02c9579e1ef414051e541ea28005cc6b525b554e46ab1d1abd11cc959f48e74
7
- data.tar.gz: dbc43339aeaad12be683b260e4cfcac8aa70f9fbf3a23be136b161790ce4468c8711f1e45c2fd608257df4bf867427e7cf3f8b8e52b0100e218c6de6a85dce1c
6
+ metadata.gz: 2b0ed377abc9501ce2a0fa19cc17d547e02498673ac15265ceac38555bf1fa241996e52c7dc5af337677a3436cfeb2209dd47f75fe70757bea96cebca21194e1
7
+ data.tar.gz: 1bd498dc182950382bfe3dca7fb5056bf6e2ff5760161ee8edf5481ef3c444a35ca7d4c51fd682b5d62f8e6c30aec787297cf537daba0956b8aefcc810b6172c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,109 @@
2
2
 
3
3
  The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
4
4
 
5
+ ## 0.24.0 — 2026-07-28
6
+
7
+ The **smooth-load convention**, opt-in per app. Pages materialize behind the
8
+ current one and present themselves with a view transition — navigation renders
9
+ exactly once, with no stale-preview flash. Modifies an existing primitive:
10
+ `layouts/studio/_head` gains the convention partial and a configurable nav
11
+ spinner minimum.
12
+
13
+ **One ungated visual change on the bump, opt-in or not:** `.turbo-progress-bar`
14
+ in `engine.css` restyles every consumer's Turbo progress bar from Turbo's
15
+ default blue to the app's theme CTA color (3px, `var(--color-cta)` — defined in
16
+ both themes by the theme resolver, and the rule wins Turbo's injected-first
17
+ stylesheet cascade). Everything else is inert until an app sets
18
+ `Studio.smooth_load = true`.
19
+
20
+ ### Added
21
+
22
+ - **Smooth-load convention (opt-in)** — `Studio.smooth_load` (default OFF)
23
+ renders `layouts/studio/_smooth_load` from `_head`: the `view-transition`
24
+ meta (Turbo 8 wraps page swaps in `document.startViewTransition`) plus
25
+ `turbo-cache-control: no-preview`, so the next page materializes behind the
26
+ current one and presents itself — navigation renders exactly once. An app
27
+ with known multi-second pages should fix those before opting in (no-preview
28
+ holds the old page until the fresh response arrives).
29
+ - **`Studio.nav_spinner_min_ms`** — the nav spinner's minimum display time,
30
+ previously hardcoded to 2500 in `_head`. Default unchanged (2500); smooth-load
31
+ apps typically drop it to ~300 so fast loads never linger on a spinner.
32
+ - **Smooth-load CSS in `engine.css`** — root fade-out / rise-in view-transition
33
+ keyframes (inert until the metas render), the `.vt-pinned-header` opt-in
34
+ utility (exactly one per page — a duplicate `view-transition-name` silently
35
+ disables the transition), the theme-colored `.turbo-progress-bar` noted
36
+ above, and a `prefers-reduced-motion` kill switch app e2e suites can lean on
37
+ (`reducedMotion: "reduce"`).
38
+
39
+ ### Fixed
40
+
41
+ - **`rescue_from` order in `Studio::ErrorHandling`** — `RecordNotFound` now
42
+ resolves to `handle_not_found` (Rescuable matches last-registered first, so
43
+ the catch-all must register first). A missing record renders a real 404 from
44
+ `public/404.html` and **creates no ErrorLog row**; previously it was shadowed
45
+ by the catch-all, logged as an unexpected error, and soft-404'd (302 to root)
46
+ in production HTML. Per-host behavior changes only as each app bumps to this
47
+ release.
48
+
49
+ ## 0.23.0 — 2026-07-28
50
+
51
+ Phase D, slice 1 — the **board primitive**. The three near-identical McRitchie
52
+ Studio kanban boards (tasks / news / content) each hand-rolled the SAME SortableJS
53
+ init, the SAME optimistic move → revert → toast, and a BYTE-IDENTICAL `reorder`
54
+ controller action; the depth chart hand-rolled a within-lane variant. This slice
55
+ lifts that convergence into a neutral engine primitive, following the same seam as
56
+ the leveling modals and tricks (documented `local_assigns.fetch` locals → a
57
+ page-level Alpine factory in an `_assets` partial → capability / `fx` / event hooks
58
+ → a `demo:` local for `/admin/style` → vendored JS).
59
+
60
+ **Purely additive — new engine files only, no existing primitive's default
61
+ changes. MS and TM render identically on the bump** (they do not use
62
+ `studio/board/*` yet; the board rebases are separate later deploys).
63
+
64
+ ### Added
65
+
66
+ - **`studio/board/_board`** — the board shell: a row of columns (drop zones), an
67
+ optional live `turbo_stream_from`, and a toast host. Documented `local_assigns`
68
+ locals: `columns:` `card_partial:` `card_as:` `card_locals:` `reorder_url:`
69
+ `reorder_payload:` (`:slugs` | `:ids`) `move_url:` `move_param:` `group:`
70
+ (`false` = within-zone) `id_attr:` `zone_attr:` `handle:` `filter:`
71
+ `zone_selector:` `draggable:` `empty_selector:` `live_channel:` `optimistic:`
72
+ `toasts:` `empty_label:` `header_slot:` `above_board_slot:` `fx:` `on_move_hook:`
73
+ `on_drop_hook:` `demo:`.
74
+ - **`studio/board/_column`** — one column: header + count badge + the
75
+ `.kanban-dropzone` (`id="dropzone-<key>"`, `data-<zone_attr>`) + `.kanban-empty`
76
+ empty state (the ZONE half of the identity contract).
77
+ - **`studio/board/_card_shell`** — OPTIONAL `render layout:` chrome that emits the
78
+ CARD half of the contract (`id="card-<id>"`, `.kanban-card`, `data-<id_attr>`,
79
+ `data-<zone_attr>`). Apps may hand-roll a bespoke card that satisfies the same
80
+ contract instead.
81
+ - **`studioBoard(opts)` factory — `studio/_board_assets`.** The Alpine data behind
82
+ the board, shipped at page level (a `<script>` cloned from a component template
83
+ never runs), like `levelingActionModal`. ONE factory drives BOTH shapes — it
84
+ reads `group` / `handle` / `filter` / `draggable` / `id_attr` / `zone_attr` from
85
+ opts, so the cross-column kanban and the within-lane depth chart are the same
86
+ code. Owns `initSortables`, `handleSortEnd` (cross-column move → PATCH →
87
+ optimistic revert + red-ring + toast; a same-zone drop NEVER PATCHes the zone),
88
+ `saveOrder` (reorder POST), `updateCounts`, `observeLive`,
89
+ `installExitStreamFallback`, `animateIn`, and toasts. Preserves the
90
+ `$nextTick` → `data-alpine-ready` init ordering (a documented flake fix). The
91
+ primary extension seam is the dispatched window events
92
+ `studio:board-moved` / `:board-reordered` / `:card-added` (detail
93
+ `{ record, from, to }`) — apps LISTEN, never patch the factory.
94
+ - **SortableJS v1.15.6 (MIT) VENDORED** — `app/assets/javascripts/studio/sortable.js`,
95
+ added to `config.assets.precompile` and loaded (deferred) by
96
+ `layouts/studio/_head`, mirroring the vendored canvas-confetti. CSP-safe, no CDN;
97
+ any board consumer gets `Sortable` with zero per-app wiring.
98
+ - **`Studio::Board::Rankable`** — the shared 100-gap rank read-model: a
99
+ `board_ordered` scope (`position DESC NULLS LAST, created_at DESC`),
100
+ `set_initial_position` (max + 100 within the zone), and `reposition!(ids, gap:,
101
+ direction: :desc | :asc)`.
102
+ - **`Studio::Board::Reorderable`** — the shared `reorder` controller action,
103
+ neutral param, delegating the 100-gap restamp to `Rankable#reposition!`.
104
+ - **`/admin/style` Board specimen** — the Tasks section now renders the REAL
105
+ `studio/board/board` primitive in `demo: true` mode (drag works, no POST),
106
+ above the static stage-palette reference.
107
+
5
108
  ## 0.22.0 — 2026-07-28
6
109
 
7
110
  Two additive batches for the living style guide, converged under one release.
data/README.md CHANGED
@@ -42,6 +42,16 @@ Studio.configure do |config|
42
42
  )
43
43
  config.theme_primary = "#4BAF50" # Override default violet
44
44
  config.theme_logos = ["logo.svg"]
45
+
46
+ # Smooth-load convention (default OFF). Renders the view-transition +
47
+ # no-preview metas: Turbo page swaps materialize behind the current page and
48
+ # present with a view transition, exactly one render per navigation. Fix any
49
+ # multi-second pages BEFORE opting in — no-preview holds the old page until
50
+ # the fresh response arrives.
51
+ config.smooth_load = true
52
+ # Nav spinner minimum display (default 2500). Smooth-load apps typically
53
+ # drop to ~300; keep the high floor if multi-second ops ride the spinner.
54
+ config.nav_spinner_min_ms = 300
45
55
  end
46
56
  ```
47
57
 
@@ -114,6 +124,18 @@ privileges while impersonating.
114
124
 
115
125
  ## UI Primitives
116
126
 
127
+ ### Smooth-load header pin — `.vt-pinned-header`
128
+
129
+ When `Studio.smooth_load` is on, put `vt-pinned-header` on the app's sticky
130
+ header: it gets its own named view-transition group, so page content
131
+ transitions beneath a navbar that stays put (or smoothly morphs heights).
132
+ **Exactly one element per page** — a duplicate `view-transition-name` makes
133
+ the browser silently skip the whole transition, with no error and no animation.
134
+
135
+ ```erb
136
+ <header class="sticky top-0 vt-pinned-header ...">
137
+ ```
138
+
117
139
  Render `components/emoji_swap` inside a link or button with the `group` class to
118
140
  slide between two emoji on hover and keyboard focus. The CSS ships through
119
141
  `studio_theme_css_tag`, including a reduced-motion fade fallback.
@@ -0,0 +1,11 @@
1
+ /*
2
+ SortableJS v1.15.6 — VENDORED into studio-engine (no CDN, CSP-safe).
3
+ Source: https://github.com/SortableJS/Sortable (MIT, (c) All Sortable contributors).
4
+ Minified build: https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js
5
+ Shipped as an engine asset so consumers get zero-per-app drag-and-drop with no
6
+ third-party CDN dependency. Defines the global `Sortable`; the studio/board
7
+ primitive (studio/_board_assets -> window.studioBoard) drives it. To re-vendor on
8
+ a bump: re-fetch the pinned dist file above, keep this header, bump the version.
9
+ */
10
+ /*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
11
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),c=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function p(t,e,n){t.removeEventListener(e,n,!y&&d)}function f(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,"border-top-width")),l-=h.left+parseInt(R(i,"border-left-width")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=b(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(t,!0),i=X(t)[e];o;){var r=X(o)[n];if(!("top"===n||"left"===n?r<=i:i<=r))return o;if(o===O())break;o=M(o,!1)}return!1}function B(t,e,n,o){for(var i=0,r=0,a=t.children;r<a.length;){if("none"!==a[r].style.display&&a[r]!==jt.ghost&&(o||a[r]!==jt.dragged)&&P(a[r],n.draggable,t,!1)){if(i===e)return a[r];i++}r++}return null}function F(t,e){for(var n=t.lastElementChild;n&&(n===jt.ghost||"none"===R(n,"display")||e&&!f(n,e));)n=n.previousElementSibling;return n||null}function j(t,e){var n=0;if(!t||!t.parentNode)return-1;for(;t=t.previousElementSibling;)"TEMPLATE"===t.nodeName.toUpperCase()||t===jt.clone||e&&!f(t,e)||n++;return n}function E(t){var e=0,n=0,o=O();if(t)do{var i=b(t),r=i.a,i=i.d}while(e+=t.scrollLeft*r,n+=t.scrollTop*i,t!==o&&(t=t.parentNode));return[e,n]}function M(t,e){if(!t||!t.getBoundingClientRect)return O();var n=t,o=!1;do{if(n.clientWidth<n.scrollWidth||n.clientHeight<n.scrollHeight){var i=R(n);if(n.clientWidth<n.scrollWidth&&("auto"==i.overflowX||"scroll"==i.overflowX)||n.clientHeight<n.scrollHeight&&("auto"==i.overflowY||"scroll"==i.overflowY)){if(!n.getBoundingClientRect||n===document.body)return O();if(o||e)return n;o=!0}}}while(n=n.parentNode);return O()}function S(t,e){return Math.round(t.top)===Math.round(e.top)&&Math.round(t.left)===Math.round(e.left)&&Math.round(t.height)===Math.round(e.height)&&Math.round(t.width)===Math.round(e.width)}function _(e,n){return function(){var t;m||(1===(t=arguments).length?e.call(this,t[0]):e.apply(this,t),m=setTimeout(function(){m=void 0},n))}}function H(t,e,n){t.scrollLeft+=e,t.scrollTop+=n}function C(t){var e=window.Polymer,n=window.jQuery||window.Zepto;return e&&e.dom?e.dom(t).cloneNode(!0):n?n(t).clone(!0)[0]:t.cloneNode(!0)}function T(t,e){R(t,"position","absolute"),R(t,"top",e.top),R(t,"left",e.left),R(t,"width",e.width),R(t,"height",e.height)}function x(t){R(t,"position",""),R(t,"top",""),R(t,"left",""),R(t,"width",""),R(t,"height","")}function L(n,o,i){var r={};return Array.from(n.children).forEach(function(t){var e;P(t,o.draggable,n,!1)&&!t.animated&&t!==i&&(e=X(t),r.left=Math.min(null!==(t=r.left)&&void 0!==t?t:1/0,e.left),r.top=Math.min(null!==(t=r.top)&&void 0!==t?t:1/0,e.top),r.right=Math.max(null!==(t=r.right)&&void 0!==t?t:-1/0,e.right),r.bottom=Math.max(null!==(t=r.bottom)&&void 0!==t?t:-1/0,e.bottom))}),r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}var K="Sortable"+(new Date).getTime();function A(){var e,o=[];return{captureAnimationState:function(){o=[],this.options.animation&&[].slice.call(this.el.children).forEach(function(t){var e,n;"none"!==R(t,"display")&&t!==jt.ghost&&(o.push({target:t,rect:X(t)}),e=I({},o[o.length-1].rect),!t.thisAnimationDuration||(n=b(t,!0))&&(e.top-=n.f,e.left-=n.e),t.fromRect=e)})},addAnimationState:function(t){o.push(t)},removeAnimationState:function(t){o.splice(function(t,e){for(var n in t)if(t.hasOwnProperty(n))for(var o in e)if(e.hasOwnProperty(o)&&e[o]===t[n][o])return Number(n);return-1}(o,{target:t}),1)},animateAll:function(t){var c=this;if(!this.options.animation)return clearTimeout(e),void("function"==typeof t&&t());var u=!1,d=0;o.forEach(function(t){var e=0,n=t.target,o=n.fromRect,i=X(n),r=n.prevFromRect,a=n.prevToRect,l=t.rect,s=b(n,!0);s&&(i.top-=s.f,i.left-=s.e),n.toRect=i,n.thisAnimationDuration&&S(r,i)&&!S(o,i)&&(l.top-i.top)/(l.left-i.left)==(o.top-i.top)/(o.left-i.left)&&(t=l,s=r,r=a,a=c.options,e=Math.sqrt(Math.pow(s.top-t.top,2)+Math.pow(s.left-t.left,2))/Math.sqrt(Math.pow(s.top-r.top,2)+Math.pow(s.left-r.left,2))*a.animation),S(i,o)||(n.prevFromRect=o,n.prevToRect=i,e=e||c.options.animation,c.animate(n,l,i,e)),e&&(u=!0,d=Math.max(d,e),clearTimeout(n.animationResetTimer),n.animationResetTimer=setTimeout(function(){n.animationTime=0,n.prevFromRect=null,n.fromRect=null,n.prevToRect=null,n.thisAnimationDuration=null},e),n.thisAnimationDuration=e)}),clearTimeout(e),u?e=setTimeout(function(){"function"==typeof t&&t()},d):"function"==typeof t&&t(),o=[]},animate:function(t,e,n,o){var i,r;o&&(R(t,"transition",""),R(t,"transform",""),i=(r=b(this.el))&&r.a,r=r&&r.d,i=(e.left-n.left)/(i||1),r=(e.top-n.top)/(r||1),t.animatingX=!!i,t.animatingY=!!r,R(t,"transform","translate3d("+i+"px,"+r+"px,0)"),this.forRepaintDummy=t.offsetWidth,R(t,"transition","transform "+o+"ms"+(this.options.easing?" "+this.options.easing:"")),R(t,"transform","translate3d(0,0,0)"),"number"==typeof t.animated&&clearTimeout(t.animated),t.animated=setTimeout(function(){R(t,"transition",""),R(t,"transform",""),t.animated=!1,t.animatingX=!1,t.animatingY=!1},o))}}}var N=[],W={initializeByDefault:!0},z={mount:function(e){for(var t in W)!W.hasOwnProperty(t)||t in e||(e[t]=W[t]);N.forEach(function(t){if(t.pluginName===e.pluginName)throw"Sortable: Cannot mount plugin ".concat(e.pluginName," more than once")}),N.push(e)},pluginEvent:function(e,n,o){var t=this;this.eventCanceled=!1,o.cancel=function(){t.eventCanceled=!0};var i=e+"Global";N.forEach(function(t){n[t.pluginName]&&(n[t.pluginName][i]&&n[t.pluginName][i](I({sortable:n},o)),n.options[t.pluginName]&&n[t.pluginName][e]&&n[t.pluginName][e](I({sortable:n},o)))})},initializePlugins:function(n,o,i,t){for(var e in N.forEach(function(t){var e=t.pluginName;(n.options[e]||t.initializeByDefault)&&((t=new t(n,o,n.options)).sortable=n,t.options=n.options,n[e]=t,a(i,t.defaults))}),n.options){var r;n.options.hasOwnProperty(e)&&(void 0!==(r=this.modifyOption(n,e,n.options[e]))&&(n.options[e]=r))}},getEventProperties:function(e,n){var o={};return N.forEach(function(t){"function"==typeof t.eventProperties&&a(o,t.eventProperties.call(n[t.pluginName],e))}),o},modifyOption:function(e,n,o){var i;return N.forEach(function(t){e[t.pluginName]&&t.optionListeners&&"function"==typeof t.optionListeners[n]&&(i=t.optionListeners[n].call(e[t.pluginName],o))}),i}};function G(t){var e=t.sortable,n=t.rootEl,o=t.name,i=t.targetEl,r=t.cloneEl,a=t.toEl,l=t.fromEl,s=t.oldIndex,c=t.newIndex,u=t.oldDraggableIndex,d=t.newDraggableIndex,h=t.originalEvent,p=t.putSortable,f=t.extraEventProperties;if(e=e||n&&n[K]){var g,m=e.options,t="on"+o.charAt(0).toUpperCase()+o.substr(1);!window.CustomEvent||y||w?(g=document.createEvent("Event")).initEvent(o,!0,!0):g=new CustomEvent(o,{bubbles:!0,cancelable:!0}),g.to=a||n,g.from=l||n,g.item=i||n,g.clone=r,g.oldIndex=s,g.newIndex=c,g.oldDraggableIndex=u,g.newDraggableIndex=d,g.originalEvent=h,g.pullMode=p?p.lastPutMode:void 0;var v,b=I(I({},f),z.getEventProperties(o,e));for(v in b)g[v]=b[v];n&&n.dispatchEvent(g),m[t]&&m[t].call(e,g)}}function U(t,e){var n=(o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{}).evt,o=i(o,q);z.pluginEvent.bind(jt)(t,e,I({dragEl:Z,parentEl:$,ghostEl:Q,rootEl:J,nextEl:tt,lastDownEl:et,cloneEl:nt,cloneHidden:ot,dragStarted:mt,putSortable:ct,activeSortable:jt.active,originalEvent:n,oldIndex:it,oldDraggableIndex:at,newIndex:rt,newDraggableIndex:lt,hideGhostForTarget:Xt,unhideGhostForTarget:Yt,cloneNowHidden:function(){ot=!0},cloneNowShown:function(){ot=!1},dispatchSortableEvent:function(t){V({sortable:e,name:t,originalEvent:n})}},o))}var q=["evt"];function V(t){G(I({putSortable:ct,cloneEl:nt,targetEl:Z,rootEl:J,oldIndex:it,oldDraggableIndex:at,newIndex:rt,newDraggableIndex:lt},t))}var Z,$,Q,J,tt,et,nt,ot,it,rt,at,lt,st,ct,ut,dt,ht,pt,ft,gt,mt,vt,bt,yt,wt,Dt=!1,Et=!1,St=[],_t=!1,Ct=!1,Tt=[],xt=!1,Ot=[],Mt="undefined"!=typeof document,At=c,Nt=w||y?"cssFloat":"float",It=Mt&&!n&&!c&&"draggable"in document.createElement("div"),Pt=function(){if(Mt){if(y)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),kt=function(t,e){var n=R(t),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),i=B(t,0,e),r=B(t,1,e),a=i&&R(i),l=r&&R(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+X(i).width,t=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+X(r).width;if("flex"===n.display)return"column"===n.flexDirection||"column-reverse"===n.flexDirection?"vertical":"horizontal";if("grid"===n.display)return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(i&&a.float&&"none"!==a.float){e="left"===a.float?"left":"right";return!r||"both"!==l.clear&&l.clear!==e?"horizontal":"vertical"}return i&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||o<=s&&"none"===n[Nt]||r&&"none"===n[Nt]&&o<s+t)?"vertical":"horizontal"},Rt=function(t){function l(r,a){return function(t,e,n,o){var i=t.options.group.name&&e.options.group.name&&t.options.group.name===e.options.group.name;if(null==r&&(a||i))return!0;if(null==r||!1===r)return!1;if(a&&"clone"===r)return r;if("function"==typeof r)return l(r(t,e,n,o),a)(t,e,n,o);e=(a?t:e).options.group.name;return!0===r||"string"==typeof r&&r===e||r.join&&-1<r.indexOf(e)}}var e={},n=t.group;n&&"object"==o(n)||(n={name:n}),e.name=n.name,e.checkPull=l(n.pull,!0),e.checkPut=l(n.put),e.revertClone=n.revertClone,t.group=e},Xt=function(){!Pt&&Q&&R(Q,"display","none")},Yt=function(){!Pt&&Q&&R(Q,"display","")};Mt&&!n&&document.addEventListener("click",function(t){if(Et)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),Et=!1},!0);function Bt(t){if(Z){t=t.touches?t.touches[0]:t;var e=(i=t.clientX,r=t.clientY,St.some(function(t){var e=t[K].options.emptyInsertThreshold;if(e&&!F(t)){var n=X(t),o=i>=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))<n)return;this._onDragStart(t,!0)}Q&&(r?(r.e+=a-(ht||0),r.f+=l-(pt||0)):r={a:1,b:0,c:0,d:1,e:a,f:l},r="matrix(".concat(r.a,",").concat(r.b,",").concat(r.c,",").concat(r.d,",").concat(r.e,",").concat(r.f,")"),R(Q,"webkitTransform",r),R(Q,"mozTransform",r),R(Q,"msTransform",r),R(Q,"transform",r),ht=a,pt=l,dt=i),t.cancelable&&t.preventDefault()}},_appendGhost:function(){if(!Q){var t=this.options.fallbackOnBody?document.body:J,e=X(Z,!0,At,!0,t),n=this.options;if(At){for(wt=t;"static"===R(wt,"position")&&"none"===R(wt,"transform")&&wt!==document;)wt=wt.parentNode;wt!==document.body&&wt!==document.documentElement?(wt===document&&(wt=O()),e.top+=wt.scrollTop,e.left+=wt.scrollLeft):wt=O(),Tt=E(wt)}k(Q=Z.cloneNode(!0),n.ghostClass,!1),k(Q,n.fallbackClass,!0),k(Q,n.dragClass,!0),R(Q,"transition",""),R(Q,"transform",""),R(Q,"box-sizing","border-box"),R(Q,"margin",0),R(Q,"top",e.top),R(Q,"left",e.left),R(Q,"width",e.width),R(Q,"height",e.height),R(Q,"opacity","0.8"),R(Q,"position",At?"absolute":"fixed"),R(Q,"zIndex","100000"),R(Q,"pointerEvents","none"),jt.ghost=Q,t.appendChild(Q),R(Q,"transform-origin",ft/parseInt(Q.style.width)*100+"% "+gt/parseInt(Q.style.height)*100+"%")}},_onDragStart:function(t,e){var n=this,o=t.dataTransfer,i=n.options;U("dragStart",this,{evt:t}),jt.eventCanceled?this._onDrop():(U("setupClone",this),jt.eventCanceled||((nt=C(Z)).removeAttribute("id"),nt.draggable=!1,nt.style["will-change"]="",this._hideClone(),k(nt,this.options.chosenClass,!1),jt.clone=nt),n.cloneId=Wt(function(){U("clone",n),jt.eventCanceled||(n.options.removeCloneOnHide||J.insertBefore(nt,Z),n._hideClone(),V({sortable:n,name:"clone"}))}),e||k(Z,i.dragClass,!0),e?(Et=!0,n._loopId=setInterval(n._emulateDragOver,50)):(p(document,"mouseup",n._onDrop),p(document,"touchend",n._onDrop),p(document,"touchcancel",n._onDrop),o&&(o.effectAllowed="move",i.setData&&i.setData.call(n,o,Z)),h(document,"drop",n),R(Z,"transform","translateZ(0)")),Dt=!0,n._dragStartId=Wt(n._dragStarted.bind(n,e,t)),h(document,"selectstart",n),mt=!0,window.getSelection().removeAllRanges(),u&&R(document.body,"user-select","none"))},_onDragOver:function(n){var o,i,r,t,e,a=this.el,l=n.target,s=this.options,c=s.group,u=jt.active,d=st===c,h=s.sort,p=ct||u,f=this,g=!1;if(!xt){if(void 0!==n.preventDefault&&n.cancelable&&n.preventDefault(),l=P(l,s.draggable,a,!0),O("dragOver"),jt.eventCanceled)return g;if(Z.contains(n.target)||l.animated&&l.animatingX&&l.animatingY||f._ignoreWhileAnimating===l)return A(!1);if(Et=!1,u&&!s.disabled&&(d?h||(i=$!==J):ct===this||(this.lastPutMode=st.checkPull(this,u,Z,n))&&c.checkPut(this,u,Z,n))){if(r="vertical"===this._getDirection(n,l),o=X(Z),O("dragOverValid"),jt.eventCanceled)return g;if(i)return $=J,M(),this._hideClone(),O("revert"),jt.eventCanceled||(tt?J.insertBefore(Z,tt):J.appendChild(Z)),A(!0);var m=F(a,s.draggable);if(m&&(S=n,c=r,x=X(F((E=this).el,E.options.draggable)),E=L(E.el,E.options,Q),!(c?S.clientX>E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY<C.top&&t.clientX<C.right:t.clientY<_.top-10||t.clientY<C.bottom&&t.clientX<C.left)){var v=B(a,0,s,!0);if(v===Z)return A(!1);if(D=X(l=v),!1!==Ht(J,a,Z,o,l,D,n,!1))return M(),a.insertBefore(Z,v),$=a,N(),A(!0)}else if(l.parentNode===a){var b,y,w,D=X(l),E=Z.parentNode!==a,S=(S=Z.animated&&Z.toRect||o,x=l.animated&&l.toRect||D,_=(e=r)?S.left:S.top,t=e?S.right:S.bottom,C=e?S.width:S.height,v=e?x.left:x.top,S=e?x.right:x.bottom,x=e?x.width:x.height,!(_===v||t===S||_+C/2===v+x/2)),_=r?"top":"left",C=Y(l,"top","top")||Y(Z,"top","top"),v=C?C.scrollTop:void 0;if(vt!==l&&(y=D[_],_t=!1,Ct=!S&&s.invertSwap||E),0!==(b=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,t=o?n.top:n.left,o=o?n.bottom:n.right,n=!1;if(!a)if(l&&yt<c*i){if(_t=!_t&&(1===bt?t+c*r/2<s:s<o-c*r/2)?!0:_t)n=!0;else if(1===bt?s<t+yt:o-yt<s)return-bt}else if(t+c*(1-i)/2<s&&s<o-c*(1-i)/2)return function(t){return j(Z)<j(t)?1:-1}(e);if((n=n||a)&&(s<t+c*r/2||o-c*r/2<s))return t+c/2<s?1:-1;return 0}(n,l,D,r,S?1:s.swapThreshold,null==s.invertedSwapThreshold?s.swapThreshold:s.invertedSwapThreshold,Ct,vt===l)))for(var T=j(Z);(w=$.children[T-=b])&&("none"===R(w,"display")||w===Q););if(0===b||w===l)return A(!1);bt=b;var x=(vt=l).nextElementSibling,E=!1,S=Ht(J,a,Z,o,l,D,n,E=1===b);if(!1!==S)return 1!==S&&-1!==S||(E=1===S),xt=!0,setTimeout(Kt,30),M(),E&&!x?a.appendChild(Z):l.parentNode.insertBefore(Z,E?x:l),C&&H(C,0,v-C.scrollTop),$=Z.parentNode,void 0===y||Ct||(yt=Math.abs(y-X(l)[_])),N(),A(!0)}}else{if(m===Z)return A(!1);if((l=m&&a===n.target?m:l)&&(D=X(l)),!1!==Ht(J,a,Z,o,l,D,n,!!l))return M(),m&&m.nextSibling?a.insertBefore(Z,m.nextSibling):a.appendChild(Z),$=a,N(),A(!0)}if(a.contains(Z))return A(!1)}return!1}function O(t,e){U(t,f,I({evt:n,isOwner:d,axis:r?"vertical":"horizontal",revert:i,dragRect:o,targetRect:D,canSort:h,fromSortable:p,target:l,completed:A,onMove:function(t,e){return Ht(J,a,Z,o,t,X(t),n,e)},changed:N},e))}function M(){O("dragOverAnimationCapture"),f.captureAnimationState(),f!==p&&p.captureAnimationState()}function A(t){return O("dragOverCompleted",{insertion:t}),t&&(d?u._hideClone():u._showClone(f),f!==p&&(k(Z,(ct||u).options.ghostClass,!1),k(Z,s.ghostClass,!0)),ct!==f&&f!==jt.active?ct=f:f===jt.active&&ct&&(ct=null),p===f&&(f._ignoreWhileAnimating=l),f.animateAll(function(){O("dragOverAnimationComplete"),f._ignoreWhileAnimating=null}),f!==p&&(p.animateAll(),p._ignoreWhileAnimating=null)),(l===Z&&!Z.animated||l===a&&!l.animated)&&(vt=null),s.dragoverBubble||n.rootEl||l===document||(Z.parentNode[K]._isOutsideThisEl(n.target),t||Bt(n)),!s.dragoverBubble&&n.stopPropagation&&n.stopPropagation(),g=!0}function N(){rt=j(Z),lt=j(Z,s.draggable),V({sortable:f,name:"change",toEl:a,newIndex:rt,newDraggableIndex:lt,originalEvent:n})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){p(document,"mousemove",this._onTouchMove),p(document,"touchmove",this._onTouchMove),p(document,"pointermove",this._onTouchMove),p(document,"dragover",Bt),p(document,"mousemove",Bt),p(document,"touchmove",Bt)},_offUpEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._onDrop),p(t,"touchend",this._onDrop),p(t,"pointerup",this._onDrop),p(t,"pointercancel",this._onDrop),p(t,"touchcancel",this._onDrop),p(document,"selectstart",this)},_onDrop:function(t){var e=this.el,n=this.options;rt=j(Z),lt=j(Z,n.draggable),U("drop",this,{evt:t}),$=Z&&Z.parentNode,rt=j(Z),lt=j(Z,n.draggable),jt.eventCanceled||(_t=Ct=Dt=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),zt(this.cloneId),zt(this._dragStartId),this.nativeDraggable&&(p(document,"drop",this),p(e,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),u&&R(document.body,"user-select",""),R(Z,"transform",""),t&&(mt&&(t.cancelable&&t.preventDefault(),n.dropBubble||t.stopPropagation()),Q&&Q.parentNode&&Q.parentNode.removeChild(Q),(J===$||ct&&"clone"!==ct.lastPutMode)&&nt&&nt.parentNode&&nt.parentNode.removeChild(nt),Z&&(this.nativeDraggable&&p(Z,"dragend",this),Lt(Z),Z.style["will-change"]="",mt&&!Dt&&k(Z,(ct||this).options.ghostClass,!1),k(Z,this.options.chosenClass,!1),V({sortable:this,name:"unchoose",toEl:$,newIndex:null,newDraggableIndex:null,originalEvent:t}),J!==$?(0<=rt&&(V({rootEl:$,name:"add",toEl:$,fromEl:J,originalEvent:t}),V({sortable:this,name:"remove",toEl:$,originalEvent:t}),V({rootEl:$,name:"sort",toEl:$,fromEl:J,originalEvent:t}),V({sortable:this,name:"sort",toEl:$,originalEvent:t})),ct&&ct.save()):rt!==it&&0<=rt&&(V({sortable:this,name:"update",toEl:$,originalEvent:t}),V({sortable:this,name:"sort",toEl:$,originalEvent:t})),jt.active&&(null!=rt&&-1!==rt||(rt=it,lt=at),V({sortable:this,name:"end",toEl:$,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){U("nulling",this),J=Z=$=Q=tt=nt=et=ot=ut=dt=mt=rt=lt=it=at=vt=bt=ct=st=jt.dragged=jt.ghost=jt.clone=jt.active=null,Ot.forEach(function(t){t.checked=!0}),Ot.length=ht=pt=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":Z&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move");t.cancelable&&t.preventDefault()}(t));break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],n=this.el.children,o=0,i=n.length,r=this.options;o<i;o++)P(t=n[o],r.draggable,this.el,!1)&&e.push(t.getAttribute(r.dataIdAttr)||function(t){var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;for(;n--;)o+=e.charCodeAt(n);return o.toString(36)}(t));return e},sort:function(t,e){var n={},o=this.el;this.toArray().forEach(function(t,e){e=o.children[e];P(e,this.options.draggable,o,!1)&&(n[t]=e)},this),e&&this.captureAnimationState(),t.forEach(function(t){n[t]&&(o.removeChild(n[t]),o.appendChild(n[t]))}),e&&this.animateAll()},save:function(){var t=this.options.store;t&&t.set&&t.set(this)},closest:function(t,e){return P(t,e||this.options.draggable,this.el,!1)},option:function(t,e){var n=this.options;if(void 0===e)return n[t];var o=z.modifyOption(this,t,e);n[t]=void 0!==o?o:e,"group"===t&&Rt(n)},destroy:function(){U("destroy",this);var t=this.el;t[K]=null,p(t,"mousedown",this._onTapStart),p(t,"touchstart",this._onTapStart),p(t,"pointerdown",this._onTapStart),this.nativeDraggable&&(p(t,"dragover",this),p(t,"dragenter",this)),Array.prototype.forEach.call(t.querySelectorAll("[draggable]"),function(t){t.removeAttribute("draggable")}),this._onDrop(),this._disableDelayedDragEvents(),St.splice(St.indexOf(this.el),1),this.el=t=null},_hideClone:function(){ot||(U("hideClone",this),jt.eventCanceled||(R(nt,"display","none"),this.options.removeCloneOnHide&&nt.parentNode&&nt.parentNode.removeChild(nt),ot=!0))},_showClone:function(t){"clone"===t.lastPutMode?ot&&(U("showClone",this),jt.eventCanceled||(Z.parentNode!=J||this.options.group.revertClone?tt?J.insertBefore(nt,tt):J.appendChild(nt):J.insertBefore(nt,Z),this.options.group.revertClone&&this.animate(Z,nt),R(nt,"display",""),ot=!1)):this._hideClone()}},Mt&&h(document,"touchmove",function(t){(jt.active||Dt)&&t.cancelable&&t.preventDefault()}),jt.utils={on:h,off:p,css:R,find:D,is:function(t,e){return!!P(t,e,t,!1)},extend:function(t,e){if(t&&e)for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t},throttle:_,closest:P,toggleClass:k,clone:C,index:j,nextTick:Wt,cancelNextTick:zt,detectDirection:kt,getChild:B,expando:K},jt.get=function(t){return t[K]},jt.mount=function(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];(e=e[0].constructor===Array?e[0]:e).forEach(function(t){if(!t.prototype||!t.prototype.constructor)throw"Sortable: Mounted plugin must be a constructor function, not ".concat({}.toString.call(t));t.utils&&(jt.utils=I(I({},jt.utils),t.utils)),z.mount(t)})},jt.create=function(t,e){return new jt(t,e)};var Gt,Ut,qt,Vt,Zt,$t,Qt=[],Jt=!(jt.version="1.15.6");function te(){Qt.forEach(function(t){clearInterval(t.pid)}),Qt=[]}function ee(){clearInterval($t)}var ne,oe=_(function(n,t,e,o){if(t.scroll){var i,r=(n.touches?n.touches[0]:n).clientX,a=(n.touches?n.touches[0]:n).clientY,l=t.scrollSensitivity,s=t.scrollSpeed,c=O(),u=!1;Ut!==e&&(Ut=e,te(),Gt=t.scroll,i=t.scrollFn,!0===Gt&&(Gt=M(e,!0)));var d=0,h=Gt;do{var p=h,f=X(p),g=f.top,m=f.bottom,v=f.left,b=f.right,y=f.width,w=f.height,D=void 0,E=void 0,S=p.scrollWidth,_=p.scrollHeight,C=R(p),T=p.scrollLeft,f=p.scrollTop,E=p===c?(D=y<S&&("auto"===C.overflowX||"scroll"===C.overflowX||"visible"===C.overflowX),w<_&&("auto"===C.overflowY||"scroll"===C.overflowY||"visible"===C.overflowY)):(D=y<S&&("auto"===C.overflowX||"scroll"===C.overflowX),w<_&&("auto"===C.overflowY||"scroll"===C.overflowY)),T=D&&(Math.abs(b-r)<=l&&T+y<S)-(Math.abs(v-r)<=l&&!!T),f=E&&(Math.abs(m-a)<=l&&f+w<_)-(Math.abs(g-a)<=l&&!!f);if(!Qt[d])for(var x=0;x<=d;x++)Qt[x]||(Qt[x]={});Qt[d].vx==T&&Qt[d].vy==f&&Qt[d].el===p||(Qt[d].el=p,Qt[d].vx=T,Qt[d].vy=f,clearInterval(Qt[d].pid),0==T&&0==f||(u=!0,Qt[d].pid=setInterval(function(){o&&0===this.layer&&jt.active._onTouchMove(Zt);var t=Qt[this.layer].vy?Qt[this.layer].vy*s:0,e=Qt[this.layer].vx?Qt[this.layer].vx*s:0;"function"==typeof i&&"continue"!==i.call(jt.dragged.parentNode[K],e,t,n,Zt,Qt[this.layer].el)||H(Qt[this.layer].el,e,t)}.bind({layer:d}),24))),d++}while(t.bubbleScroll&&h!==c&&(h=M(h,!1)));Jt=u}},30),n=function(t){var e=t.originalEvent,n=t.putSortable,o=t.dragEl,i=t.activeSortable,r=t.dispatchSortableEvent,a=t.hideGhostForTarget,t=t.unhideGhostForTarget;e&&(i=n||i,a(),e=e.changedTouches&&e.changedTouches.length?e.changedTouches[0]:e,e=document.elementFromPoint(e.clientX,e.clientY),t(),i&&!i.el.contains(e)&&(r("spill"),this.onSpill({dragEl:o,putSortable:n})))};function ie(){}function re(){}ie.prototype={startIndex:null,dragStart:function(t){t=t.oldDraggableIndex;this.startIndex=t},onSpill:function(t){var e=t.dragEl,n=t.putSortable;this.sortable.captureAnimationState(),n&&n.captureAnimationState();t=B(this.sortable.el,this.startIndex,this.options);t?this.sortable.el.insertBefore(e,t):this.sortable.el.appendChild(e),this.sortable.animateAll(),n&&n.animateAll()},drop:n},a(ie,{pluginName:"revertOnSpill"}),re.prototype={onSpill:function(t){var e=t.dragEl,t=t.putSortable||this.sortable;t.captureAnimationState(),e.parentNode&&e.parentNode.removeChild(e),t.animateAll()},drop:n},a(re,{pluginName:"removeOnSpill"});var ae,le,se,ce,ue,de=[],he=[],pe=!1,fe=!1,ge=!1;function me(n,o){he.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)})}function ve(){de.forEach(function(t){t!==se&&t.parentNode&&t.parentNode.removeChild(t)})}return jt.mount(new function(){function t(){for(var t in this.defaults={scroll:!0,forceAutoScrollFallback:!1,scrollSensitivity:30,scrollSpeed:10,bubbleScroll:!0},this)"_"===t.charAt(0)&&"function"==typeof this[t]&&(this[t]=this[t].bind(this))}return t.prototype={dragStarted:function(t){t=t.originalEvent;this.sortable.nativeDraggable?h(document,"dragover",this._handleAutoScroll):this.options.supportPointer?h(document,"pointermove",this._handleFallbackAutoScroll):t.touches?h(document,"touchmove",this._handleFallbackAutoScroll):h(document,"mousemove",this._handleFallbackAutoScroll)},dragOverCompleted:function(t){t=t.originalEvent;this.options.dragOverBubble||t.rootEl||this._handleAutoScroll(t)},drop:function(){this.sortable.nativeDraggable?p(document,"dragover",this._handleAutoScroll):(p(document,"pointermove",this._handleFallbackAutoScroll),p(document,"touchmove",this._handleFallbackAutoScroll),p(document,"mousemove",this._handleFallbackAutoScroll)),ee(),te(),clearTimeout(m),m=void 0},nulling:function(){Zt=Ut=Gt=Jt=$t=qt=Vt=null,Qt.length=0},_handleFallbackAutoScroll:function(t){this._handleAutoScroll(t,!0)},_handleAutoScroll:function(e,n){var o,i=this,r=(e.touches?e.touches[0]:e).clientX,a=(e.touches?e.touches[0]:e).clientY,t=document.elementFromPoint(r,a);Zt=e,n||this.options.forceAutoScrollFallback||w||y||u?(oe(e,this.options,t,n),o=M(t,!0),!Jt||$t&&r===qt&&a===Vt||($t&&ee(),$t=setInterval(function(){var t=M(document.elementFromPoint(r,a),!0);t!==o&&(o=t,te()),oe(e,i.options,t,n)},10),qt=r,Vt=a)):this.options.bubbleScroll&&M(t,!0)!==O()?oe(e,this.options,M(t,!1),!1):te()}},a(t,{pluginName:"scroll",initializeByDefault:!0})}),jt.mount(re,ie),jt.mount(new function(){function t(){this.defaults={swapClass:"sortable-swap-highlight"}}return t.prototype={dragStart:function(t){t=t.dragEl;ne=t},dragOverValid:function(t){var e=t.completed,n=t.target,o=t.onMove,i=t.activeSortable,r=t.changed,a=t.cancel;i.options.swap&&(t=this.sortable.el,i=this.options,n&&n!==t&&(t=ne,ne=!1!==o(n)?(k(n,i.swapClass,!0),n):null,t&&t!==ne&&k(t,i.swapClass,!1)),r(),e(!0),a())},drop:function(t){var e,n,o=t.activeSortable,i=t.putSortable,r=t.dragEl,a=i||this.sortable,l=this.options;ne&&k(ne,l.swapClass,!1),ne&&(l.swap||i&&i.options.swap)&&r!==ne&&(a.captureAnimationState(),a!==o&&o.captureAnimationState(),n=ne,t=(e=r).parentNode,l=n.parentNode,t&&l&&!t.isEqualNode(n)&&!l.isEqualNode(e)&&(i=j(e),r=j(n),t.isEqualNode(l)&&i<r&&r++,t.insertBefore(n,t.children[i]),l.insertBefore(e,l.children[r])),a.animateAll(),a!==o&&o.animateAll())},nulling:function(){ne=null}},a(t,{pluginName:"swap",eventProperties:function(){return{swapItem:ne}}})}),jt.mount(new function(){function t(o){for(var t in this)"_"===t.charAt(0)&&"function"==typeof this[t]&&(this[t]=this[t].bind(this));o.options.avoidImplicitDeselect||(o.options.supportPointer?h(document,"pointerup",this._deselectMultiDrag):(h(document,"mouseup",this._deselectMultiDrag),h(document,"touchend",this._deselectMultiDrag))),h(document,"keydown",this._checkKeyDown),h(document,"keyup",this._checkKeyUp),this.defaults={selectedClass:"sortable-selected",multiDragKey:null,avoidImplicitDeselect:!1,setData:function(t,e){var n="";de.length&&le===o?de.forEach(function(t,e){n+=(e?", ":"")+t.textContent}):n=e.textContent,t.setData("Text",n)}}}return t.prototype={multiDragKeyDown:!1,isMultiDrag:!1,delayStartGlobal:function(t){t=t.dragEl;se=t},delayEnded:function(){this.isMultiDrag=~de.indexOf(se)},setupClone:function(t){var e=t.sortable,t=t.cancel;if(this.isMultiDrag){for(var n=0;n<de.length;n++)he.push(C(de[n])),he[n].sortableIndex=de[n].sortableIndex,he[n].draggable=!1,he[n].style["will-change"]="",k(he[n],this.options.selectedClass,!1),de[n]===se&&k(he[n],this.options.chosenClass,!1);e._hideClone(),t()}},clone:function(t){var e=t.sortable,n=t.rootEl,o=t.dispatchSortableEvent,t=t.cancel;this.isMultiDrag&&(this.options.removeCloneOnHide||de.length&&le===e&&(me(!0,n),o("clone"),t()))},showClone:function(t){var e=t.cloneNowShown,n=t.rootEl,t=t.cancel;this.isMultiDrag&&(me(!1,n),he.forEach(function(t){R(t,"display","")}),e(),ue=!1,t())},hideClone:function(t){var e=this,n=(t.sortable,t.cloneNowHidden),t=t.cancel;this.isMultiDrag&&(he.forEach(function(t){R(t,"display","none"),e.options.removeCloneOnHide&&t.parentNode&&t.parentNode.removeChild(t)}),n(),ue=!0,t())},dragStartGlobal:function(t){t.sortable;!this.isMultiDrag&&le&&le.multiDrag._deselectMultiDrag(),de.forEach(function(t){t.sortableIndex=j(t)}),de=de.sort(function(t,e){return t.sortableIndex-e.sortableIndex}),ge=!0},dragStarted:function(t){var e,n=this,t=t.sortable;this.isMultiDrag&&(this.options.sort&&(t.captureAnimationState(),this.options.animation&&(de.forEach(function(t){t!==se&&R(t,"position","absolute")}),e=X(se,!1,!0,!0),de.forEach(function(t){t!==se&&T(t,e)}),pe=fe=!0)),t.animateAll(function(){pe=fe=!1,n.options.animation&&de.forEach(function(t){x(t)}),n.options.sort&&ve()}))},dragOver:function(t){var e=t.target,n=t.completed,t=t.cancel;fe&&~de.indexOf(e)&&(n(!1),t())},revert:function(t){var n,o,e=t.fromSortable,i=t.rootEl,r=t.sortable,a=t.dragRect;1<de.length&&(de.forEach(function(t){r.addAnimationState({target:t,rect:fe?X(t):a}),x(t),t.fromRect=a,e.removeAnimationState(t)}),fe=!1,n=!this.options.removeCloneOnHide,o=i,de.forEach(function(t,e){e=o.children[t.sortableIndex+(n?Number(e):0)];e?o.insertBefore(t,e):o.appendChild(t)}))},dragOverCompleted:function(t){var e,n=t.sortable,o=t.isOwner,i=t.insertion,r=t.activeSortable,a=t.parentEl,l=t.putSortable,t=this.options;i&&(o&&r._hideClone(),pe=!1,t.animation&&1<de.length&&(fe||!o&&!r.options.sort&&!l)&&(e=X(se,!1,!0,!0),de.forEach(function(t){t!==se&&(T(t,e),a.appendChild(t))}),fe=!0),o||(fe||ve(),1<de.length?(o=ue,r._showClone(n),r.options.animation&&!ue&&o&&he.forEach(function(t){r.addAnimationState({target:t,rect:ce}),t.fromRect=ce,t.thisAnimationDuration=null})):r._showClone(n)))},dragOverAnimationCapture:function(t){var e=t.dragRect,n=t.isOwner,t=t.activeSortable;de.forEach(function(t){t.thisAnimationDuration=null}),t.options.animation&&!n&&t.multiDrag.isMultiDrag&&(ce=a({},e),e=b(se,!0),ce.top-=e.f,ce.left-=e.e)},dragOverAnimationComplete:function(){fe&&(fe=!1,ve())},drop:function(t){var o,i,r,a,n,e,l,s=t.originalEvent,c=t.rootEl,u=t.parentEl,d=t.sortable,h=t.dispatchSortableEvent,p=t.oldIndex,t=t.putSortable,f=t||this.sortable;s&&(o=this.options,i=u.children,ge||(o.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),k(se,o.selectedClass,!~de.indexOf(se)),~de.indexOf(se)?(de.splice(de.indexOf(se),1),ae=null,G({sortable:d,rootEl:c,name:"deselect",targetEl:se,originalEvent:s})):(de.push(se),G({sortable:d,rootEl:c,name:"select",targetEl:se,originalEvent:s}),s.shiftKey&&ae&&d.el.contains(ae)?(r=j(ae),a=j(se),~r&&~a&&r!==a&&function(){for(var e,t=r<a?(e=r,a):(e=a,r+1),n=o.filter;e<t;e++)~de.indexOf(i[e])||P(i[e],o.draggable,u,!1)&&(n&&("function"==typeof n?n.call(d,s,i[e],d):n.split(",").some(function(t){return P(i[e],t.trim(),u,!1)}))||(k(i[e],o.selectedClass,!0),de.push(i[e]),G({sortable:d,rootEl:c,name:"select",targetEl:i[e],originalEvent:s})))}()):ae=se,le=f)),ge&&this.isMultiDrag&&(fe=!1,(u[K].options.sort||u!==c)&&1<de.length&&(n=X(se),e=j(se,":not(."+this.options.selectedClass+")"),!pe&&o.animation&&(se.thisAnimationDuration=null),f.captureAnimationState(),pe||(o.animation&&(se.fromRect=n,de.forEach(function(t){var e;t.thisAnimationDuration=null,t!==se&&(e=fe?X(t):n,t.fromRect=e,f.addAnimationState({target:t,rect:e}))})),ve(),de.forEach(function(t){i[e]?u.insertBefore(t,i[e]):u.appendChild(t),e++}),p===j(se)&&(l=!1,de.forEach(function(t){t.sortableIndex!==j(t)&&(l=!0)}),l&&(h("update"),h("sort")))),de.forEach(function(t){x(t)}),f.animateAll()),le=f),(c===u||t&&"clone"!==t.lastPutMode)&&he.forEach(function(t){t.parentNode&&t.parentNode.removeChild(t)}))},nullingGlobal:function(){this.isMultiDrag=ge=!1,he.length=0},destroyGlobal:function(){this._deselectMultiDrag(),p(document,"pointerup",this._deselectMultiDrag),p(document,"mouseup",this._deselectMultiDrag),p(document,"touchend",this._deselectMultiDrag),p(document,"keydown",this._checkKeyDown),p(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function(t){if(!(void 0!==ge&&ge||le!==this.sortable||t&&P(t.target,this.options.draggable,this.sortable.el,!1)||t&&0!==t.button))for(;de.length;){var e=de[0];k(e,this.options.selectedClass,!1),de.shift(),G({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:e,originalEvent:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},a(t,{pluginName:"multiDrag",utils:{select:function(t){var e=t.parentNode[K];e&&e.options.multiDrag&&!~de.indexOf(t)&&(le&&le!==e&&(le.multiDrag._deselectMultiDrag(),le=e),k(t,e.options.selectedClass,!0),de.push(t))},deselect:function(t){var e=t.parentNode[K],n=de.indexOf(t);e&&e.options.multiDrag&&~n&&(k(t,e.options.selectedClass,!1),de.splice(n,1))}},eventProperties:function(){var n=this,o=[],i=[];return de.forEach(function(t){var e;o.push({multiDragElement:t,index:t.sortableIndex}),e=fe&&t!==se?-1:fe?j(t,":not(."+n.options.selectedClass+")"):j(t),i.push({multiDragElement:t,index:e})}),{items:r(de),clones:[].concat(he),oldIndicies:o,newIndicies:i}},optionListeners:{multiDragKey:function(t){return"ctrl"===(t=t.toLowerCase())?t="Control":1<t.length&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})}),jt});
@@ -180,3 +180,62 @@
180
180
  @utility btn-lg {
181
181
  @apply px-8 py-3 text-base rounded-xl shadow-sm;
182
182
  }
183
+
184
+ /* -- Smooth-load convention ------------------------------------------------
185
+ Paired with layouts/studio/_smooth_load.html.erb (the Studio.smooth_load
186
+ metas that opt Turbo into document.startViewTransition). These rules are
187
+ inert until an app renders those metas, so shipping them in the auto-bundled
188
+ layer costs nothing. The outgoing page fades while the incoming one rises in
189
+ underneath — quick enough to read as polish, not as a wait. */
190
+ ::view-transition-old(root) {
191
+ animation: vt-page-out 160ms cubic-bezier(0.4, 0, 1, 1) both;
192
+ }
193
+
194
+ ::view-transition-new(root) {
195
+ animation: vt-page-in 220ms cubic-bezier(0, 0, 0.2, 1) both;
196
+ }
197
+
198
+ @keyframes vt-page-out {
199
+ to {
200
+ opacity: 0;
201
+ }
202
+ }
203
+
204
+ @keyframes vt-page-in {
205
+ from {
206
+ opacity: 0;
207
+ transform: translateY(8px);
208
+ }
209
+ to {
210
+ opacity: 1;
211
+ transform: none;
212
+ }
213
+ }
214
+
215
+ /* Opt-in header pin: put this on the app's sticky header so it gets its own
216
+ named transition group, lifted out of the root snapshot — page content
217
+ transitions beneath a navbar that stays put (or smoothly morphs heights).
218
+ EXACTLY ONE element per page: a duplicate view-transition-name makes the
219
+ browser silently skip the whole transition. */
220
+ @utility vt-pinned-header {
221
+ view-transition-name: studio-header;
222
+ }
223
+
224
+ /* Turbo's progress bar is the slow-load feedback under no-preview (the old
225
+ page holds until the fresh response). Theme it so every app's bar matches
226
+ its brand instead of Turbo's default. */
227
+ .turbo-progress-bar {
228
+ height: 3px;
229
+ background: var(--color-cta);
230
+ }
231
+
232
+ /* Reduced motion: keep the single-render swap, drop the choreography. Also
233
+ what the apps' e2e suites lean on (reducedMotion: "reduce") for stable
234
+ clicks. */
235
+ @media (prefers-reduced-motion: reduce) {
236
+ ::view-transition-group(*),
237
+ ::view-transition-old(*),
238
+ ::view-transition-new(*) {
239
+ animation: none !important;
240
+ }
241
+ }
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Studio
4
+ module Board
5
+ # Shared `reorder` action for board controllers. The three McRitchie Studio
6
+ # kanban controllers (tasks / news / content) each carried a BYTE-IDENTICAL
7
+ # `reorder` — guard the incoming id array, restamp the column with 100-gaps
8
+ # INSIDE `rescue_and_log` (so a failure lands in ErrorLog — backend write
9
+ # discipline), render `{ success: true }`, and rescue to a 422. This concern is
10
+ # that action, neutral: the including controller declares its model, id column,
11
+ # and the incoming param, and the ranking rule itself is delegated to the model's
12
+ # Studio::Board::Rankable#reposition! (one place owns the 100-gap math). This is
13
+ # THE designated shared write action, so it owns the ErrorLog logging here rather
14
+ # than leaving each host to re-add it.
15
+ #
16
+ # class TasksController < ApplicationController
17
+ # include Studio::Board::Reorderable
18
+ # board_reorderable model: Task, id_attr: :slug, param: :slugs
19
+ # end
20
+ #
21
+ # # Route it however the app names the endpoint:
22
+ # post "tasks/reorder", to: "tasks#reorder"
23
+ #
24
+ # The POST body the studio/board factory sends is `{ <param>: [...ids],
25
+ # zone: "<zone-key>" }`; this action reads only `<param>` (the ordered id list)
26
+ # and ignores the advisory `zone`, so a kanban and a depth-chart board share it.
27
+ module Reorderable
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ class_attribute :board_reorder_model, instance_accessor: false
32
+ class_attribute :board_reorder_id_attr, instance_accessor: false, default: :slug
33
+ class_attribute :board_reorder_param, instance_accessor: false, default: :slugs
34
+ class_attribute :board_reorder_gap, instance_accessor: false, default: 100
35
+ class_attribute :board_reorder_direction, instance_accessor: false, default: :desc
36
+ end
37
+
38
+ class_methods do
39
+ # Configure the reorder action for this controller's board.
40
+ def board_reorderable(model:, id_attr: :slug, param: :slugs, gap: 100, direction: :desc)
41
+ self.board_reorder_model = model
42
+ self.board_reorder_id_attr = id_attr
43
+ self.board_reorder_param = param
44
+ self.board_reorder_gap = gap
45
+ self.board_reorder_direction = direction
46
+ end
47
+ end
48
+
49
+ # POST — restamp a column from its DOM-ordered id list. Neutral param
50
+ # (`slugs` or `ids`), 100-gap, DESC by default. Mirrors the MS hand-rolled
51
+ # `reorder` exactly, INCLUDING its ErrorLog logging: the restamp runs inside
52
+ # `rescue_and_log(target: nil)` (Studio::ErrorHandling), which captures a
53
+ # failure to ErrorLog and RE-RAISES to the outer 422 net below — the "every
54
+ # write path logs" discipline, owned here since this is the one shared write
55
+ # action. `respond_to?` guards it so a host whose ApplicationController somehow
56
+ # lacks the concern still degrades to a bare restamp + 422 (no NoMethodError).
57
+ def reorder
58
+ model = self.class.board_reorder_model
59
+ raise "board_reorderable model not configured" unless model
60
+
61
+ param = self.class.board_reorder_param
62
+ ids = params[param]
63
+ return render(json: { error: "#{param} required" }, status: :unprocessable_entity) unless ids.is_a?(Array)
64
+
65
+ if respond_to?(:rescue_and_log)
66
+ rescue_and_log(target: nil) { board_reorder_apply(model, ids) }
67
+ else
68
+ board_reorder_apply(model, ids)
69
+ end
70
+ render json: { success: true }
71
+ rescue StandardError => e
72
+ render json: { error: e.message }, status: :unprocessable_entity
73
+ end
74
+
75
+ private
76
+
77
+ # Delegate to the model's Rankable#reposition! when available (the shared
78
+ # 100-gap rule), else fall back to the same per-id update_all loop inline so
79
+ # a model that only carries a `position` column still reorders.
80
+ def board_reorder_apply(model, ids)
81
+ id_attr = self.class.board_reorder_id_attr
82
+ gap = self.class.board_reorder_gap
83
+ direction = self.class.board_reorder_direction
84
+
85
+ if model.respond_to?(:reposition!)
86
+ model.reposition!(ids, gap: gap, direction: direction, id_attr: id_attr)
87
+ else
88
+ length = ids.length
89
+ ids.each_with_index do |id, index|
90
+ rank = direction.to_sym == :asc ? (index + 1) * gap : (length - index) * gap
91
+ model.where(id_attr => id).update_all(position: rank)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -3,8 +3,15 @@ module Studio
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
6
+ # ORDER IS LOAD-BEARING — ActiveSupport::Rescuable resolves handlers with
7
+ # `reverse_each`, so the LAST matching rescue_from registered wins. The
8
+ # catch-all must therefore be declared FIRST and the specific handlers
9
+ # after it, or StandardError shadows every one of them: RecordNotFound
10
+ # rendered "Something went wrong" and wrote an ErrorLog row on every 404,
11
+ # fleet-wide, while handle_not_found sat unreachable. Add new specific
12
+ # handlers BELOW this line, never above it.
7
13
  rescue_from StandardError, with: :handle_unexpected_error
14
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
8
15
 
9
16
  before_action :require_authentication
10
17
 
@@ -195,12 +202,33 @@ module Studio
195
202
  raise e
196
203
  end
197
204
 
198
- # Layer 1: Catch-all for RecordNotFound — 404 redirect, no logging.
205
+ # Layer 1: RecordNotFound — Rails' own 404, never an ErrorLog row.
206
+ #
207
+ # This handler exists for exactly ONE reason: to stop `handle_unexpected_error`
208
+ # from claiming a RecordNotFound and writing an ErrorLog row for it. It
209
+ # deliberately renders NOTHING and re-raises, which hands the exception to
210
+ # `ActionDispatch::PublicExceptions` — the same path an app with no rescue_from
211
+ # at all takes. Registering the handler and re-raising is therefore not a
212
+ # no-op: matching here is what keeps the catch-all's `create_error_log` from
213
+ # running (a raise inside a rescue_from handler propagates out; Rescuable does
214
+ # not re-dispatch it into another handler).
215
+ #
216
+ # Why nothing is rendered here. The method was unreachable from the initial
217
+ # commit until the rescue_from order was fixed (see the note in `included`),
218
+ # so whatever it did had never actually run — consumers have been served
219
+ # Rails' standard 404 all along. Anything rendered here would therefore be a
220
+ # NEW fleet-wide behaviour, not a restoration: an earlier attempt at this fix
221
+ # rendered public/404.html and silently flipped `Accept: */*` clients (curl,
222
+ # uptime monitors, crawlers, link previewers) from `404 text/html` to
223
+ # `404 application/json`, changed the JSON body shape, and dropped the
224
+ # DebugExceptions backtrace in development. Re-raising keeps every one of
225
+ # those intact, along with `public/404.<locale>.html` and any
226
+ # `config.exceptions_app` a host configures.
227
+ #
228
+ # The bug being fixed is the ErrorLog pollution and the "Something went wrong"
229
+ # copy — never the status code, the format, or the body.
199
230
  def handle_not_found(exception)
200
- respond_to do |format|
201
- format.html { redirect_to root_path, alert: "Not found" }
202
- format.json { render json: { error: "Not found" }, status: :not_found }
203
- end
231
+ raise exception
204
232
  end
205
233
 
206
234
  # Layer 1: Catch-all for unexpected errors — log + friendly response.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Studio
4
+ module Board
5
+ # Shared 100-gap rank read-model for board-orderable records — the kanban
6
+ # columns (McRitchie Studio tasks / news / content) and the depth-chart lanes.
7
+ # A record carries an integer `position`; the board renders by `board_ordered`
8
+ # (highest position on top), and a drag-reorder restamps the whole column with
9
+ # 100-gaps via `reposition!`. Ranking is ZONE-SCOPED: a fresh rank is computed
10
+ # within the record's zone (a kanban `stage`, a depth-chart position group), so
11
+ # every column ranks independently and 100-spacing leaves room for the next
12
+ # drag insert without a full re-stamp.
13
+ #
14
+ # This is the read/rank half of the board primitive; the write half is
15
+ # Studio::Board::Reorderable (the controller action that calls reposition!).
16
+ #
17
+ # class Task < ApplicationRecord
18
+ # include Studio::Board::Rankable
19
+ # before_create :set_initial_position # seeds the genesis rank
20
+ # end
21
+ #
22
+ # class DepthChartEntry < ApplicationRecord
23
+ # include Studio::Board::Rankable
24
+ # self.board_zone_attr = :position_group # a lane, not a kanban stage
25
+ # end
26
+ module Rankable
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ # The column the record ranks WITHIN. Default matches the three MS kanban
31
+ # boards (stage); a within-lane board overrides it, and `nil` ranks globally.
32
+ class_attribute :board_zone_attr, instance_accessor: false, default: :stage
33
+
34
+ # Board order: highest `position` first (freshest/top card wins), NULLS last,
35
+ # then newest by created_at as a stable tiebreak. Arel.sql: the fragment is a
36
+ # fixed literal, not user input.
37
+ scope :board_ordered, -> { order(Arel.sql("position DESC NULLS LAST, created_at DESC")) }
38
+ end
39
+
40
+ # Seed the genesis rank on create: max(position) within this record's zone plus
41
+ # a 100 gap. A no-op when `position` is already set, so an explicit rank is
42
+ # never clobbered. Wire from the host via `before_create :set_initial_position`.
43
+ def set_initial_position
44
+ return if position.present?
45
+
46
+ self.position = self.class.board_next_position(zone_value_for_rank)
47
+ end
48
+
49
+ # This record's zone value (nil when the model ranks globally).
50
+ def zone_value_for_rank
51
+ attr = self.class.board_zone_attr
52
+ attr && respond_to?(attr) ? public_send(attr) : nil
53
+ end
54
+
55
+ class_methods do
56
+ # max(position) + gap within a zone (or globally when the zone value / attr
57
+ # is blank). The genesis seed and the "bump to top on a stage move" both read
58
+ # from here, so the 100-gap rule lives in one place.
59
+ def board_next_position(zone_value = nil, gap: 100)
60
+ scope = board_zone_attr && zone_value ? where(board_zone_attr => zone_value) : all
61
+ (scope.maximum(:position) || 0) + gap
62
+ end
63
+
64
+ # Restamp a column top-to-bottom in ONE pass. `ids` arrive in DOM order (top
65
+ # first). Under the `position DESC` board sort the TOP card must hold the
66
+ # HIGHEST rank, so with `direction: :desc` (the board default) the first id
67
+ # gets the largest value and each next one drops by `gap` — 100-spacing that
68
+ # leaves gaps for the next drag insert. `direction: :asc` ranks the other way
69
+ # (first id smallest) for an ascending board. This is exactly the per-id
70
+ # `update_all(position: ...)` loop the MS tasks/news/content reorder actions
71
+ # each ran by hand, lifted into the model. Returns the ids it stamped.
72
+ def reposition!(ids, gap: 100, direction: :desc, id_attr: primary_key)
73
+ ids = Array(ids)
74
+ length = ids.length
75
+ ids.each_with_index do |id, index|
76
+ rank = direction.to_sym == :asc ? (index + 1) * gap : (length - index) * gap
77
+ where(id_attr => id).update_all(position: rank)
78
+ end
79
+ ids
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,4 +1,4 @@
1
- <%# locals: (text:, scheme: "neutral") %>
1
+ <%# locals: (text:, scheme: "neutral", data_board_count: nil) %>
2
2
  <%#
3
3
  Stage roles (stage-fresh ... stage-closed) form a shared pipeline palette
4
4
  used by News and Content (and any future pipelines). Mapping:
@@ -32,4 +32,6 @@
32
32
  else "bg-surface-alt text-body border-subtle"
33
33
  end
34
34
  %>
35
- <span class="badge <%= scheme_classes %>"><%= text %></span>
35
+ <%# data_board_count wires the badge as a studio/board count target (updateCounts sets
36
+ its textContent) — additive + opt-in, so every existing caller renders identically. %>
37
+ <span class="badge <%= scheme_classes %>"<% if local_assigns[:data_board_count] %> data-board-count="<%= data_board_count %>"<% end %>><%= text %></span>
@@ -10,6 +10,7 @@
10
10
  <meta name="apple-mobile-web-app-capable" content="yes">
11
11
  <%= csrf_meta_tags %>
12
12
  <%= csp_meta_tag %>
13
+ <%= render "layouts/studio/smooth_load" %>
13
14
  <link rel="icon" type="image/png" href="/favicon.png">
14
15
 
15
16
  <%= yield :head %>
@@ -28,6 +29,12 @@
28
29
  is unchanged and rides the same global. %>
29
30
  <%= javascript_include_tag "studio/canvas_confetti", "data-turbo-track": "reload" %>
30
31
  <%= javascript_include_tag "studio/studio_confetti", "data-turbo-track": "reload" %>
32
+ <%# SortableJS is VENDORED into the engine (studio/sortable.js) and shipped through
33
+ the asset pipeline — no CDN, CSP-safe (same-origin :self). Defines the global
34
+ `Sortable`; the studio/board primitive (studio/_board_assets -> window.studioBoard)
35
+ drives it for drag-reorder kanban AND depth-chart boards. Loaded here (deferred)
36
+ so any board consumer has it with zero per-app wiring, mirroring canvas_confetti. %>
37
+ <%= javascript_include_tag "studio/sortable", defer: true, "data-turbo-track": "reload" %>
31
38
  <script>
32
39
  window.fireSuccessConfetti = function() {
33
40
  if (typeof confetti === 'undefined') return;
@@ -40,9 +47,10 @@
40
47
  </script>
41
48
  <script>
42
49
  // Nav spinner — scale morph between theme toggle and loading spinner
43
- // Minimum display time prevents quick flashes
50
+ // Minimum display time prevents quick flashes; per-app via
51
+ // Studio.nav_spinner_min_ms (smooth-load apps drop it to ~300).
44
52
  window._navSpinnerShownAt = 0;
45
- window._navSpinnerMinMs = 2500;
53
+ window._navSpinnerMinMs = <%= Studio.nav_spinner_min_ms.to_i %>;
46
54
  window.showNavSpinner = function() {
47
55
  window._navSpinnerShownAt = Date.now();
48
56
  document.querySelectorAll('.nav-toggle-icon').forEach(function(e) { e.style.opacity = '0'; e.style.transform = 'scale(0) rotate(90deg)'; });