katalyst-kpop 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +113 -0
- data/app/assets/builds/katalyst/kpop.css +117 -0
- data/app/assets/builds/katalyst/kpop.min.js +1 -0
- data/app/assets/config/kpop.js +1 -0
- data/app/assets/javascripts/controllers/kpop_controller.js +69 -0
- data/app/assets/javascripts/controllers/scrim_controller.js +162 -0
- data/app/assets/javascripts/katalyst/kpop.js +9 -0
- data/app/assets/stylesheets/katalyst/kpop/_index.scss +2 -0
- data/app/assets/stylesheets/katalyst/kpop/_kpop.scss +133 -0
- data/app/assets/stylesheets/katalyst/kpop/_scrim.scss +16 -0
- data/app/assets/stylesheets/katalyst/kpop.scss +1 -0
- data/app/helpers/kpop/modal.rb +98 -0
- data/app/helpers/kpop_helper.rb +39 -0
- data/app/helpers/scrim_helper.rb +12 -0
- data/app/views/layouts/kpop.html.erb +4 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +2 -0
- data/lib/katalyst/kpop/engine.rb +36 -0
- data/lib/katalyst/kpop/version.rb +7 -0
- data/lib/katalyst/kpop.rb +9 -0
- data/lib/tasks/yarn.rake +23 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bfe556211c8cb27976e75cf940c7ae065fc6fbfb31f8a9fb2c78ec6e3d36db4d
|
4
|
+
data.tar.gz: 7014e09c2697ff7e0d0093f67068b1583a21c8bdb165222df6b990e63869eac0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7affe4e861d299c11391520a71adf5d7a9fa4e1af699d7bd9938b0c784784c531992fd68d7d8ea14c58a974b2f6a52ff6e021bcab5811874730e006b1d7d31e9
|
7
|
+
data.tar.gz: d087f450b624e8dc6e4c2426672eb697e52f6e5fe3bbac402f2b71458d0c35ce8f956a60b6c6446f9a6bce4100fbb33865dd4af8550ce12fe0342540a59add1d
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# kpop
|
2
|
+
|
3
|
+
Modals driven by `@hotwire/turbo` frame navigation.
|
4
|
+
|
5
|
+
kpop requires `@hotwire/turbo` and `@hotwire/stimulus` to be installed and configured correctly to be used.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install gem
|
10
|
+
```bash
|
11
|
+
# Gemfile
|
12
|
+
|
13
|
+
$ bundle add "katalyst-kpop"
|
14
|
+
```
|
15
|
+
|
16
|
+
kpop supports installation of javascript dependencies with either import maps or yarn.
|
17
|
+
|
18
|
+
### Stimulus controllers
|
19
|
+
|
20
|
+
If you are using asset pipeline and import maps then the stimulus controllers
|
21
|
+
for modals and scrim will be automatically available without configuration.
|
22
|
+
|
23
|
+
### Stylesheets
|
24
|
+
|
25
|
+
Import stylesheets through using SASS using asset pipeline:
|
26
|
+
|
27
|
+
```scss
|
28
|
+
// app/assets/stylesheets/application.scss
|
29
|
+
|
30
|
+
@use "katalyst/kpop";
|
31
|
+
```
|
32
|
+
|
33
|
+
You can also load a precompiled version from the gem directly:
|
34
|
+
|
35
|
+
```erb
|
36
|
+
<%# app/views/layouts/application.html.erb #>
|
37
|
+
|
38
|
+
<%= stylesheet_link_tag "katalyst/kpop" %>
|
39
|
+
```
|
40
|
+
|
41
|
+
### Yarn
|
42
|
+
|
43
|
+
If you are not using import maps, you can add the yarn package to your project:
|
44
|
+
|
45
|
+
```bash
|
46
|
+
$ yarn add "@katalyst-interactive/kpop"
|
47
|
+
```
|
48
|
+
|
49
|
+
### Import kpop styles
|
50
|
+
```css
|
51
|
+
/* application.scss */
|
52
|
+
|
53
|
+
@import "~@katalyst-interactive/kpop";
|
54
|
+
```
|
55
|
+
|
56
|
+
### Import kpop stimulus controllers
|
57
|
+
```js
|
58
|
+
/* application.js */
|
59
|
+
import kpop from "@katalyst-interactive/kpop"
|
60
|
+
application.load(kpop)
|
61
|
+
```
|
62
|
+
|
63
|
+
## Usage
|
64
|
+
|
65
|
+
kpop provides helpers to add a basic scrim and modal target frame. These should be placed inside the body:
|
66
|
+
```html
|
67
|
+
<body>
|
68
|
+
<%= scrim_tag %>
|
69
|
+
<%= kpop_frame_tag do %>
|
70
|
+
<%= yield :kpop %>
|
71
|
+
<% end %>
|
72
|
+
</body>
|
73
|
+
```
|
74
|
+
|
75
|
+
### Show a modal
|
76
|
+
|
77
|
+
To show a modal you need to add content to the kpop turbo frame. You can do this in several ways:
|
78
|
+
1. Use `content_for :kpop` in an HTML response to inject content into the kpop frame (see `yield :kpop` above)
|
79
|
+
2. Use `layout "kpop"` in your controller to wrap your turbo response in a kpop frame
|
80
|
+
|
81
|
+
You can generate a link that will cause a modal to show using the `kpop_link_to` helper.
|
82
|
+
|
83
|
+
`kpop_link_to`'s are similar to a `link_to` in rails, but it will navigate to the given URL within the modal turbo
|
84
|
+
frame. The targeted action will need to generate content in a `kpop_frame_tag`, e.g. using `layout "kpop"`.
|
85
|
+
|
86
|
+
```html
|
87
|
+
<!-- app/views/homepage/index.html.erb -->
|
88
|
+
<%= modal_link_to "click to open modal", modal_path("example") %>
|
89
|
+
```
|
90
|
+
|
91
|
+
```html
|
92
|
+
<!-- app/views/modals/show.html.erb -->
|
93
|
+
<%= render_kpop(title: "Modal title") do %>
|
94
|
+
Modal content
|
95
|
+
<% end %>
|
96
|
+
```
|
97
|
+
|
98
|
+
Note that, because kpop modals render in a turbo frame, if you want to navigate the parent frame you will need to use
|
99
|
+
`target: "_top"` on your links and forms.
|
100
|
+
|
101
|
+
## Development
|
102
|
+
|
103
|
+
Releases need to be distributed to rubygems.org and npmjs.org. To do this, you need to have accounts with both providers
|
104
|
+
and be added as a collaborator to the kpop gem and npm packages.
|
105
|
+
|
106
|
+
1. Update the version in `package.json` and `lib/katalyst/kpop/version.rb`
|
107
|
+
2. Ensure that `rake` passes (format and tests)
|
108
|
+
3. Tag a release and push to rubygems.org by running `rake release`
|
109
|
+
4. Push the new version to npmjs.org by running `yarn publish`
|
110
|
+
|
111
|
+
## License
|
112
|
+
|
113
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,117 @@
|
|
1
|
+
.kpop-container {
|
2
|
+
display: none;
|
3
|
+
position: fixed;
|
4
|
+
left: 0;
|
5
|
+
top: 0;
|
6
|
+
right: 0;
|
7
|
+
bottom: 0;
|
8
|
+
justify-content: center;
|
9
|
+
align-items: center;
|
10
|
+
z-index: 1000;
|
11
|
+
pointer-events: none;
|
12
|
+
}
|
13
|
+
.kpop-container > * {
|
14
|
+
pointer-events: auto;
|
15
|
+
}
|
16
|
+
|
17
|
+
.kpop-modal {
|
18
|
+
position: relative;
|
19
|
+
overflow: hidden;
|
20
|
+
display: grid;
|
21
|
+
grid-template-areas: "title-bar" "header" "content" "footer";
|
22
|
+
grid-template-rows: auto auto 1fr auto;
|
23
|
+
border: 1px solid black;
|
24
|
+
border-radius: 0.5rem;
|
25
|
+
background-color: white;
|
26
|
+
min-width: 35rem;
|
27
|
+
max-width: 52rem;
|
28
|
+
min-height: 0;
|
29
|
+
max-height: 80vh;
|
30
|
+
}
|
31
|
+
.kpop-modal .kpop-title-bar {
|
32
|
+
grid-area: title-bar;
|
33
|
+
display: flex;
|
34
|
+
background: #344055;
|
35
|
+
color: white;
|
36
|
+
}
|
37
|
+
.kpop-modal .kpop-title-bar .kpop-title {
|
38
|
+
padding: 1rem 1.5rem;
|
39
|
+
flex-grow: 1;
|
40
|
+
}
|
41
|
+
.kpop-modal .kpop-title-bar .kpop-close {
|
42
|
+
background: none;
|
43
|
+
border: none;
|
44
|
+
color: white;
|
45
|
+
cursor: pointer;
|
46
|
+
display: block;
|
47
|
+
font-size: 2rem;
|
48
|
+
font-weight: bold;
|
49
|
+
padding: 0 0.75rem;
|
50
|
+
text-decoration: none;
|
51
|
+
}
|
52
|
+
.kpop-modal .kpop-header {
|
53
|
+
grid-area: header;
|
54
|
+
}
|
55
|
+
.kpop-modal .kpop-content {
|
56
|
+
grid-area: content;
|
57
|
+
display: flex;
|
58
|
+
flex-direction: column;
|
59
|
+
overflow: auto;
|
60
|
+
}
|
61
|
+
.kpop-modal .kpop-footer {
|
62
|
+
grid-area: footer;
|
63
|
+
background: white;
|
64
|
+
border-top: 1px solid black;
|
65
|
+
padding: 1rem 1.5rem;
|
66
|
+
}
|
67
|
+
.kpop-modal .kpop-buttons {
|
68
|
+
display: flex;
|
69
|
+
gap: 0.5rem;
|
70
|
+
justify-content: space-between;
|
71
|
+
}
|
72
|
+
|
73
|
+
.kpop-modal.iframe .kpop-content {
|
74
|
+
overflow: unset;
|
75
|
+
}
|
76
|
+
.kpop-modal.iframe iframe {
|
77
|
+
height: 80vh;
|
78
|
+
width: 52rem;
|
79
|
+
flex-grow: 1;
|
80
|
+
overflow: scroll;
|
81
|
+
}
|
82
|
+
|
83
|
+
@media (max-width: 600px), (max-height: 600px) {
|
84
|
+
.kpop-modal {
|
85
|
+
max-width: unset;
|
86
|
+
min-width: unset;
|
87
|
+
width: 100%;
|
88
|
+
height: 100%;
|
89
|
+
max-height: 100vh;
|
90
|
+
border-radius: 0;
|
91
|
+
border: none;
|
92
|
+
}
|
93
|
+
.kpop-modal.iframe iframe {
|
94
|
+
width: 100%;
|
95
|
+
height: 100%;
|
96
|
+
}
|
97
|
+
.kpop-buttons {
|
98
|
+
flex-direction: column-reverse;
|
99
|
+
text-align: center;
|
100
|
+
}
|
101
|
+
}
|
102
|
+
.scrim {
|
103
|
+
position: fixed;
|
104
|
+
top: 0;
|
105
|
+
bottom: 0;
|
106
|
+
left: 0;
|
107
|
+
right: 0;
|
108
|
+
background: rgba(0, 0, 0, 0.6);
|
109
|
+
z-index: -1;
|
110
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-end;
|
111
|
+
opacity: 0;
|
112
|
+
}
|
113
|
+
|
114
|
+
.scrim[data-scrim-open-value=true] {
|
115
|
+
opacity: 1;
|
116
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-start;
|
117
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
function e(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function t(e){return e.charAt(0).toUpperCase()+e.slice(1)}function r(e,t){const r=s(e);return Array.from(r.reduce(((e,r)=>(function(e,t){const r=e[t];return Array.isArray(r)?r:[]}(r,t).forEach((t=>e.add(t))),e)),new Set))}function n(e,t){return s(e).reduce(((e,r)=>(e.push(...function(e,t){const r=e[t];return r?Object.keys(r).map((e=>[e,r[e]])):[]}(r,t)),e)),[])}function s(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}function i(e){return e.reduce(((e,[t,r])=>Object.assign(Object.assign({},e),{[t]:r})),{})}function o([t,r],n){return function(t){const r=`${s=t.token,s.replace(/([A-Z])/g,((e,t)=>`-${t.toLowerCase()}`))}-value`,n=function(e){const t=function(e){const t=a(e.typeObject.type);if(!t)return;const r=c(e.typeObject.default);if(t!==r){const n=e.controller?`${e.controller}.${e.token}`:e.token;throw new Error(`The specified default value for the Stimulus Value "${n}" must match the defined type "${t}". The provided default value of "${e.typeObject.default}" is of type "${r}".`)}return t}({controller:e.controller,token:e.token,typeObject:e.typeDefinition}),r=c(e.typeDefinition),n=a(e.typeDefinition),s=t||r||n;if(s)return s;const i=e.controller?`${e.controller}.${e.typeDefinition}`:e.token;throw new Error(`Unknown value type "${i}" for "${e.token}" value`)}(t);var s;return{type:n,key:r,name:e(r),get defaultValue(){return function(e){const t=a(e);if(t)return u[t];const r=e.default;return void 0!==r?r:e}(t.typeDefinition)},get hasCustomDefaultValue(){return void 0!==c(t.typeDefinition)},reader:l[n],writer:d[n]||d.default}}({controller:n,token:t,typeDefinition:r})}function a(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function c(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e((function(){this.a.call(this)}));t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})(),Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End"},i("abcdefghijklmnopqrstuvwxyz".split("").map((e=>[e,e])))),i("0123456789".split("").map((e=>[e,e]))));const u={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:""},l={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${c(t)}"`);return t},boolean:e=>!("0"==e||"false"==String(e).toLowerCase()),number:e=>Number(e),object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${c(t)}"`);return t},string:e=>e},d={default:function(e){return`${e}`},array:h,object:h};function h(e){return JSON.stringify(e)}class p{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:r={},prefix:n=this.identifier,bubbles:s=!0,cancelable:i=!0}={}){const o=new CustomEvent(n?`${n}:${e}`:e,{detail:r,bubbles:s,cancelable:i});return t.dispatchEvent(o),o}}p.blessings=[function(e){return r(e,"classes").reduce(((e,r)=>{return Object.assign(e,{[`${n=r}Class`]:{get(){const{classes:e}=this;if(e.has(n))return e.get(n);{const t=e.getAttributeName(n);throw new Error(`Missing attribute "${t}"`)}}},[`${n}Classes`]:{get(){return this.classes.getAll(n)}},[`has${t(n)}Class`]:{get(){return this.classes.has(n)}}});var n}),{})},function(e){return r(e,"targets").reduce(((e,r)=>{return Object.assign(e,{[`${n=r}Target`]:{get(){const e=this.targets.find(n);if(e)return e;throw new Error(`Missing target element "${n}" for "${this.identifier}" controller`)}},[`${n}Targets`]:{get(){return this.targets.findAll(n)}},[`has${t(n)}Target`]:{get(){return this.targets.has(n)}}});var n}),{})},function(e){const r=n(e,"values"),s={valueDescriptorMap:{get(){return r.reduce(((e,t)=>{const r=o(t,this.identifier),n=this.data.getAttributeNameForKey(r.key);return Object.assign(e,{[n]:r})}),{})}}};return r.reduce(((e,r)=>Object.assign(e,function(e,r){const n=o(e,r),{key:s,name:i,reader:a,writer:c}=n;return{[i]:{get(){const e=this.data.get(s);return null!==e?a(e):n.defaultValue},set(e){void 0===e?this.data.delete(s):this.data.set(s,c(e))}},[`has${t(i)}`]:{get(){return this.data.has(s)||n.hasCustomDefaultValue}}}}(r))),s)},function(n){return r(n,"outlets").reduce(((r,n)=>Object.assign(r,function(r){const n=(s=r,e(s.replace(/--/g,"-").replace(/__/g,"_")));var s;return{[`${n}Outlet`]:{get(){const e=this.outlets.find(r);if(e){const t=this.application.getControllerForElementAndIdentifier(e,r);if(t)return t;throw new Error(`Missing "data-controller=${r}" attribute on outlet element for "${this.identifier}" controller`)}throw new Error(`Missing outlet element "${r}" for "${this.identifier}" controller`)}},[`${n}Outlets`]:{get(){const e=this.outlets.findAll(r);return e.length>0?e.map((e=>{const t=this.application.getControllerForElementAndIdentifier(e,r);if(t)return t;console.warn(`The provided outlet element is missing the outlet controller "${r}" for "${this.identifier}"`,e)})).filter((e=>e)):[]}},[`${n}OutletElement`]:{get(){const e=this.outlets.find(r);if(e)return e;throw new Error(`Missing outlet element "${r}" for "${this.identifier}" controller`)}},[`${n}OutletElements`]:{get(){return this.outlets.findAll(r)}},[`has${t(n)}Outlet`]:{get(){return this.outlets.has(r)}}}}(n))),{})}],p.targets=[],p.outlets=[],p.values={};class f extends p{static values={open:Boolean,captive:Boolean,zIndex:Number};static showScrim({dismiss:e=!0,zIndex:t,top:r}={}){return window.dispatchEvent(new CustomEvent("scrim:request:show",{cancelable:!0,detail:{captive:!e,zIndex:t,top:r}}))}static hideScrim(){return window.dispatchEvent(new CustomEvent("scrim:request:hide",{cancelable:!0}))}connect(){this.defaultZIndexValue=this.zIndexValue,this.defaultCaptiveValue=this.captiveValue}show(e){if(this.openValue&&this.hide(e),this.openValue)return;this.openValue=!0;if(this.dispatch("show",{bubbles:!0,cancelable:!0}).defaultPrevented)return this.openValue=!1,void e.preventDefault();this.#e(e.detail)}hide(e){if(!this.openValue)return;this.openValue=!1;if(this.dispatch("hide",{bubbles:!0,cancelable:!0}).defaultPrevented)return this.openValue=!0,void e.preventDefault();this.#t()}dismiss(e){this.captiveValue||this.hide(e)}escape(e){"Escape"!==e.key||this.captiveValue||e.defaultPrevented||this.hide(e)}disconnect(){super.disconnect()}#e({captive:e=this.defaultCaptiveValue,zIndex:t=this.defaultZIndexValue,top:r=window.scrollY}){this.captiveValue=e,this.zIndexValue=t,this.scrollY=r,this.previousPosition=document.body.style.position,this.previousTop=document.body.style.top,this.element.style.zIndex=this.zIndexValue,document.body.style.top=`-${r}px`,document.body.style.position="fixed"}#t(){this.captiveValue=this.defaultCaptiveValue,this.zIndexValue=this.defaultZIndexValue,g(this.element,"z-index",null),g(document.body,"position",null),g(document.body,"top",null),window.scrollTo(0,this.scrollY),delete this.scrollY,delete this.previousPosition,delete this.previousTop}}function g(e,t,r){r?e.style.setProperty(t,r):e.style.removeProperty(t)}class b extends p{static targets=["content","closeButton"];static values={open:Boolean};contentTargetConnected(){this.openValue||(f.showScrim({dismiss:this.hasCloseButtonTarget})?this.openValue=!0:this.#r())}contentTargetDisconnected(){this.hasContentTarget||(this.openValue=!1,f.hideScrim())}openValueChanged(e){this.element.style.display=e?"flex":"none"}dismiss(){if(!this.hasContentTarget||!this.openValue)return;const e=this.contentTarget.dataset.dismissUrl,t=this.contentTarget.dataset.dismissAction;e&&("replace"===t?!function(e,t){try{return`${new URL(e)}`==`${new URL(t,location.href)}`}catch{return!1}}(document.referrer,e)?history.replaceState({},"",e):history.back():window.location.href=e),this.#r()}#r(){this.element.removeAttribute("src"),this.element.innerHTML=""}}const y=[{identifier:"kpop",controllerConstructor:b},{identifier:"scrim",controllerConstructor:f}];export{b as KpopController,f as ScrimController,y as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_tree ../javascripts/controllers
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import ScrimController from "./scrim_controller";
|
3
|
+
|
4
|
+
export default class KpopController extends Controller {
|
5
|
+
static targets = ["content", "closeButton"];
|
6
|
+
static values = {
|
7
|
+
open: Boolean,
|
8
|
+
};
|
9
|
+
|
10
|
+
contentTargetConnected() {
|
11
|
+
// When switching modals a target may connect while scrim is already open
|
12
|
+
if (this.openValue) return;
|
13
|
+
|
14
|
+
if (ScrimController.showScrim({ dismiss: this.hasCloseButtonTarget })) {
|
15
|
+
this.openValue = true;
|
16
|
+
} else {
|
17
|
+
this.#clear();
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
contentTargetDisconnected() {
|
22
|
+
// When switching modals there may still be content to show
|
23
|
+
if (this.hasContentTarget) return;
|
24
|
+
|
25
|
+
this.openValue = false;
|
26
|
+
ScrimController.hideScrim();
|
27
|
+
}
|
28
|
+
|
29
|
+
openValueChanged(open) {
|
30
|
+
this.element.style.display = open ? "flex" : "none";
|
31
|
+
}
|
32
|
+
|
33
|
+
dismiss() {
|
34
|
+
if (!this.hasContentTarget || !this.openValue) return;
|
35
|
+
|
36
|
+
const dismissUrl = this.contentTarget.dataset.dismissUrl;
|
37
|
+
const dismissAction = this.contentTarget.dataset.dismissAction;
|
38
|
+
|
39
|
+
if (dismissUrl) {
|
40
|
+
if (dismissAction === "replace") {
|
41
|
+
if (isSameUrl(document.referrer, dismissUrl)) {
|
42
|
+
// if we came from the same page, send the user back
|
43
|
+
history.back();
|
44
|
+
} else {
|
45
|
+
// if we came from a different page, dismiss the modal and replace url
|
46
|
+
history.replaceState({}, "", dismissUrl);
|
47
|
+
}
|
48
|
+
} else {
|
49
|
+
// default, send the user on to the specified URL
|
50
|
+
window.location.href = dismissUrl;
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
this.#clear();
|
55
|
+
}
|
56
|
+
|
57
|
+
#clear() {
|
58
|
+
this.element.removeAttribute("src");
|
59
|
+
this.element.innerHTML = "";
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
function isSameUrl(previous, next) {
|
64
|
+
try {
|
65
|
+
return `${new URL(previous)}` === `${new URL(next, location.href)}`;
|
66
|
+
} catch {
|
67
|
+
return false;
|
68
|
+
}
|
69
|
+
}
|
@@ -0,0 +1,162 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
const DEBUG = false;
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Scrim controller wraps an element that creates a whole page layer.
|
7
|
+
* It is intended to be used behind a modal or nav drawer.
|
8
|
+
*
|
9
|
+
* If the Scrim element receives a click event, it automatically triggers "scrim:hide".
|
10
|
+
*
|
11
|
+
* You can show and hide the scrim programmatically by sending "scrim:request:show" and "scrim:request:hide" events to
|
12
|
+
* the window or by calling the provided methods.
|
13
|
+
*
|
14
|
+
* If you need to respond to the scrim showing or hiding you should subscribe to "scrim:show" and "scrim:hide".
|
15
|
+
*/
|
16
|
+
export default class ScrimController extends Controller {
|
17
|
+
static values = {
|
18
|
+
open: Boolean,
|
19
|
+
captive: Boolean,
|
20
|
+
zIndex: Number,
|
21
|
+
};
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Show the scrim element. Returns true if successful.
|
25
|
+
*/
|
26
|
+
static showScrim({
|
27
|
+
dismiss = true,
|
28
|
+
zIndex = undefined,
|
29
|
+
top = undefined,
|
30
|
+
} = {}) {
|
31
|
+
return window.dispatchEvent(
|
32
|
+
new CustomEvent("scrim:request:show", {
|
33
|
+
cancelable: true,
|
34
|
+
detail: { captive: !dismiss, zIndex: zIndex, top: top },
|
35
|
+
})
|
36
|
+
);
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Hide the scrim element. Returns true if successful.
|
41
|
+
*/
|
42
|
+
static hideScrim() {
|
43
|
+
return window.dispatchEvent(
|
44
|
+
new CustomEvent("scrim:request:hide", { cancelable: true })
|
45
|
+
);
|
46
|
+
}
|
47
|
+
|
48
|
+
connect() {
|
49
|
+
this.defaultZIndexValue = this.zIndexValue;
|
50
|
+
this.defaultCaptiveValue = this.captiveValue;
|
51
|
+
}
|
52
|
+
|
53
|
+
show(request) {
|
54
|
+
if (DEBUG) console.debug("request show scrim");
|
55
|
+
|
56
|
+
// hide the scrim before opening the new one if it's already open
|
57
|
+
if (this.openValue) this.hide(request);
|
58
|
+
|
59
|
+
// if the scrim is still open, abort
|
60
|
+
if (this.openValue) return;
|
61
|
+
|
62
|
+
// update internal state to break event cycles
|
63
|
+
this.openValue = true;
|
64
|
+
|
65
|
+
// notify listeners of pending request
|
66
|
+
const event = this.dispatch("show", { bubbles: true, cancelable: true });
|
67
|
+
|
68
|
+
// if notification was cancelled, update request and abort
|
69
|
+
if (event.defaultPrevented) {
|
70
|
+
this.openValue = false;
|
71
|
+
request.preventDefault();
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
|
75
|
+
if (DEBUG) console.debug("show scrim");
|
76
|
+
|
77
|
+
// perform show updates
|
78
|
+
this.#show(request.detail);
|
79
|
+
}
|
80
|
+
|
81
|
+
hide(request) {
|
82
|
+
if (!this.openValue) return;
|
83
|
+
|
84
|
+
if (DEBUG) console.debug("request hide scrim");
|
85
|
+
|
86
|
+
// update internal state to break event cycles
|
87
|
+
this.openValue = false;
|
88
|
+
|
89
|
+
// notify listeners of pending request
|
90
|
+
const event = this.dispatch("hide", { bubbles: true, cancelable: true });
|
91
|
+
|
92
|
+
// if notification was cancelled, update request and abort
|
93
|
+
if (event.defaultPrevented) {
|
94
|
+
this.openValue = true;
|
95
|
+
request.preventDefault();
|
96
|
+
return;
|
97
|
+
}
|
98
|
+
|
99
|
+
if (DEBUG) console.debug("hide scrim");
|
100
|
+
|
101
|
+
// update state, perform style updates
|
102
|
+
this.#hide();
|
103
|
+
}
|
104
|
+
|
105
|
+
dismiss(event) {
|
106
|
+
if (!this.captiveValue) this.hide(event);
|
107
|
+
}
|
108
|
+
|
109
|
+
escape(event) {
|
110
|
+
if (event.key === "Escape" && !this.captiveValue && !event.defaultPrevented)
|
111
|
+
this.hide(event);
|
112
|
+
}
|
113
|
+
|
114
|
+
disconnect() {
|
115
|
+
super.disconnect();
|
116
|
+
}
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Clips body to viewport size and sets the z-index
|
120
|
+
*/
|
121
|
+
#show({
|
122
|
+
captive = this.defaultCaptiveValue,
|
123
|
+
zIndex = this.defaultZIndexValue,
|
124
|
+
top = window.scrollY,
|
125
|
+
}) {
|
126
|
+
this.captiveValue = captive;
|
127
|
+
this.zIndexValue = zIndex;
|
128
|
+
this.scrollY = top;
|
129
|
+
|
130
|
+
this.previousPosition = document.body.style.position;
|
131
|
+
this.previousTop = document.body.style.top;
|
132
|
+
|
133
|
+
this.element.style.zIndex = this.zIndexValue;
|
134
|
+
document.body.style.top = `-${top}px`;
|
135
|
+
document.body.style.position = "fixed";
|
136
|
+
}
|
137
|
+
|
138
|
+
/**
|
139
|
+
* Unclips body from viewport size and unsets the z-index
|
140
|
+
*/
|
141
|
+
#hide() {
|
142
|
+
this.captiveValue = this.defaultCaptiveValue;
|
143
|
+
this.zIndexValue = this.defaultZIndexValue;
|
144
|
+
|
145
|
+
resetStyle(this.element, "z-index", null);
|
146
|
+
resetStyle(document.body, "position", null);
|
147
|
+
resetStyle(document.body, "top", null);
|
148
|
+
window.scrollTo(0, this.scrollY);
|
149
|
+
|
150
|
+
delete this.scrollY;
|
151
|
+
delete this.previousPosition;
|
152
|
+
delete this.previousTop;
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
function resetStyle(element, property, previousValue) {
|
157
|
+
if (previousValue) {
|
158
|
+
element.style.setProperty(property, previousValue);
|
159
|
+
} else {
|
160
|
+
element.style.removeProperty(property);
|
161
|
+
}
|
162
|
+
}
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import KpopController from "../controllers/kpop_controller";
|
2
|
+
import ScrimController from "../controllers/scrim_controller";
|
3
|
+
|
4
|
+
const Definitions = [
|
5
|
+
{ identifier: "kpop", controllerConstructor: KpopController },
|
6
|
+
{ identifier: "scrim", controllerConstructor: ScrimController },
|
7
|
+
];
|
8
|
+
|
9
|
+
export { Definitions as default, KpopController, ScrimController };
|
@@ -0,0 +1,133 @@
|
|
1
|
+
$title-background-color: #344055 !default;
|
2
|
+
$title-text-color: white !default;
|
3
|
+
$min-width: 35rem !default;
|
4
|
+
$max-width: 52rem !default;
|
5
|
+
$min-height: 0 !default;
|
6
|
+
$max-height: 80vh !default;
|
7
|
+
$default-padding: 1rem 1.5rem !default;
|
8
|
+
|
9
|
+
.kpop-container {
|
10
|
+
display: none;
|
11
|
+
|
12
|
+
position: fixed;
|
13
|
+
left: 0;
|
14
|
+
top: 0;
|
15
|
+
right: 0;
|
16
|
+
bottom: 0;
|
17
|
+
|
18
|
+
justify-content: center;
|
19
|
+
align-items: center;
|
20
|
+
z-index: 1000;
|
21
|
+
pointer-events: none;
|
22
|
+
|
23
|
+
> * {
|
24
|
+
pointer-events: auto;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
.kpop-modal {
|
29
|
+
position: relative;
|
30
|
+
overflow: hidden;
|
31
|
+
|
32
|
+
display: grid;
|
33
|
+
grid-template-areas:
|
34
|
+
"title-bar"
|
35
|
+
"header"
|
36
|
+
"content"
|
37
|
+
"footer";
|
38
|
+
grid-template-rows: auto auto 1fr auto;
|
39
|
+
|
40
|
+
border: 1px solid black;
|
41
|
+
border-radius: 0.5rem;
|
42
|
+
background-color: white;
|
43
|
+
|
44
|
+
min-width: $min-width;
|
45
|
+
max-width: $max-width;
|
46
|
+
min-height: $min-height;
|
47
|
+
max-height: $max-height;
|
48
|
+
|
49
|
+
.kpop-title-bar {
|
50
|
+
grid-area: title-bar;
|
51
|
+
display: flex;
|
52
|
+
background: $title-background-color;
|
53
|
+
color: $title-text-color;
|
54
|
+
|
55
|
+
.kpop-title {
|
56
|
+
padding: $default-padding;
|
57
|
+
flex-grow: 1;
|
58
|
+
}
|
59
|
+
|
60
|
+
.kpop-close {
|
61
|
+
background: none;
|
62
|
+
border: none;
|
63
|
+
color: $title-text-color;
|
64
|
+
cursor: pointer;
|
65
|
+
display: block;
|
66
|
+
font-size: 2rem;
|
67
|
+
font-weight: bold;
|
68
|
+
padding: 0 0.75rem;
|
69
|
+
text-decoration: none;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
.kpop-header {
|
74
|
+
grid-area: header;
|
75
|
+
}
|
76
|
+
|
77
|
+
.kpop-content {
|
78
|
+
grid-area: content;
|
79
|
+
display: flex;
|
80
|
+
flex-direction: column;
|
81
|
+
overflow: auto;
|
82
|
+
}
|
83
|
+
|
84
|
+
.kpop-footer {
|
85
|
+
grid-area: footer;
|
86
|
+
background: white;
|
87
|
+
border-top: 1px solid black;
|
88
|
+
padding: $default-padding;
|
89
|
+
}
|
90
|
+
|
91
|
+
.kpop-buttons {
|
92
|
+
display: flex;
|
93
|
+
gap: 0.5rem;
|
94
|
+
justify-content: space-between;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
.kpop-modal.iframe {
|
99
|
+
.kpop-content {
|
100
|
+
overflow: unset;
|
101
|
+
}
|
102
|
+
|
103
|
+
iframe {
|
104
|
+
height: $max-height;
|
105
|
+
width: $max-width;
|
106
|
+
flex-grow: 1;
|
107
|
+
overflow: scroll;
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
@media (max-width: 600px), (max-height: 600px) {
|
112
|
+
.kpop-modal {
|
113
|
+
max-width: unset;
|
114
|
+
min-width: unset;
|
115
|
+
width: 100%;
|
116
|
+
height: 100%;
|
117
|
+
max-height: 100vh;
|
118
|
+
border-radius: 0;
|
119
|
+
border: none;
|
120
|
+
}
|
121
|
+
|
122
|
+
.kpop-modal.iframe {
|
123
|
+
iframe {
|
124
|
+
width: 100%;
|
125
|
+
height: 100%;
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
.kpop-buttons {
|
130
|
+
flex-direction: column-reverse;
|
131
|
+
text-align: center;
|
132
|
+
}
|
133
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
.scrim {
|
2
|
+
position: fixed;
|
3
|
+
top: 0;
|
4
|
+
bottom: 0;
|
5
|
+
left: 0;
|
6
|
+
right: 0;
|
7
|
+
background: rgba(0, 0, 0, 0.6);
|
8
|
+
z-index: -1;
|
9
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-end;
|
10
|
+
opacity: 0;
|
11
|
+
}
|
12
|
+
|
13
|
+
.scrim[data-scrim-open-value="true"] {
|
14
|
+
opacity: 1;
|
15
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-start;
|
16
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
@use "kpop/index";
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Rails/HelperInstanceVariable
|
4
|
+
module Kpop
|
5
|
+
class Modal
|
6
|
+
delegate_missing_to :@context
|
7
|
+
|
8
|
+
def initialize(context)
|
9
|
+
@context = context
|
10
|
+
end
|
11
|
+
|
12
|
+
def render(options = {})
|
13
|
+
dom_class = options.delete(:class)
|
14
|
+
|
15
|
+
# Generate a title bar. This can be overridden by calling title_bar again.
|
16
|
+
title_bar(options) unless options.fetch(:title, "").nil?
|
17
|
+
|
18
|
+
# Render block. This may have side-effect writes to header/content/footer
|
19
|
+
# etc. If @content is set then this value will be ignored.
|
20
|
+
content = capture do
|
21
|
+
yield self
|
22
|
+
end
|
23
|
+
|
24
|
+
tag.div(class: class_names("kpop-modal", dom_class),
|
25
|
+
data: kpop_data_options(options),
|
26
|
+
**options) do
|
27
|
+
concat @title_bar
|
28
|
+
concat @header if @header.present?
|
29
|
+
concat @content.presence || tag.div(content, class: "kpop-content")
|
30
|
+
concat @footer if @footer.present?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generates a sticky title bar for the modal. Content should not be too long
|
35
|
+
# as the bar does not provide wrapping.
|
36
|
+
def title_bar(options = {}, &block)
|
37
|
+
title = options.delete(:title)
|
38
|
+
captive = options.delete(:captive)
|
39
|
+
@title_bar = tag.div(class: "kpop-title-bar", **options) do
|
40
|
+
concat(tag.span(class: "kpop-title") do
|
41
|
+
concat(block ? (yield self) : title)
|
42
|
+
end)
|
43
|
+
concat(close_icon) unless captive
|
44
|
+
end
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Generates sticky header content for the top of the modal. Content is not
|
49
|
+
# padded, if you want padding you should provide a padding class.
|
50
|
+
def header(**options, &block)
|
51
|
+
modal_content(:header, **options, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Generates content for the modal. Content is not padded, if you want
|
55
|
+
# padding you should provide a padding class.
|
56
|
+
def content(**options, &block)
|
57
|
+
modal_content(:content, **options, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generates a sticky footer element at the bottom of the modal.
|
61
|
+
# Footer is padded and contents are assumed to be buttons.
|
62
|
+
def footer(**options, &block)
|
63
|
+
modal_content(:footer, **options, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def close_icon
|
67
|
+
tag.button(
|
68
|
+
"×",
|
69
|
+
class: "kpop-close",
|
70
|
+
data: {
|
71
|
+
kpop_target: "closeButton",
|
72
|
+
action: "click->kpop#dismiss:prevent",
|
73
|
+
},
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def kpop_data_options(options)
|
80
|
+
data = options.delete(:data) || {}
|
81
|
+
data.reverse_merge(
|
82
|
+
kpop_target: "content",
|
83
|
+
dismiss_action: options.delete(:dismiss_action),
|
84
|
+
dismiss_url: options.delete(:dismiss_url),
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
def class_for(name, options)
|
89
|
+
class_names("kpop-#{name}", options.delete(:class))
|
90
|
+
end
|
91
|
+
|
92
|
+
def modal_content(name, **options, &block)
|
93
|
+
instance_variable_set("@#{name}", tag.div(class: class_for(name, options), **options, &block))
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
# rubocop:enable Rails/HelperInstanceVariable
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KpopHelper
|
4
|
+
def render_kpop(options = {}, &block)
|
5
|
+
Kpop::Modal.new(self).render(options, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def kpop_frame_tag(&block)
|
9
|
+
turbo_frame_tag "kpop",
|
10
|
+
class: "kpop-container",
|
11
|
+
data: { controller: "kpop", action: "scrim:hide@window->kpop#dismiss" } do
|
12
|
+
capture(&block) if block
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def kpop_link_to(name = nil, options = nil, html_options = nil, &block)
|
17
|
+
default_html_options = {
|
18
|
+
data: { turbo: true, turbo_frame: "kpop" },
|
19
|
+
}
|
20
|
+
if block
|
21
|
+
# Param[name] is the path for the link
|
22
|
+
link_to(name, default_html_options.deep_merge(options || {}), &block)
|
23
|
+
else
|
24
|
+
link_to(name, options, default_html_options.deep_merge(html_options || {}))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def kpop_button_to(name = nil, options = nil, html_options = nil, &block)
|
29
|
+
default_html_options = {
|
30
|
+
form: { data: { turbo: true, turbo_frame: "kpop" } },
|
31
|
+
}
|
32
|
+
button_to(name, options, default_html_options.deep_merge(html_options || {}), &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def kpop_button_close(content = nil, **options, &block)
|
36
|
+
content = block ? capture(yield) : content
|
37
|
+
tag.button content, data: { action: "click->kpop#dismiss:prevent" }, **options
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ScrimHelper
|
4
|
+
def scrim_tag(z_index: 40)
|
5
|
+
tag.div(class: "scrim", data: { controller: "scrim", scrim_z_index_value: z_index, action: <<~ACTIONS })
|
6
|
+
click->scrim#dismiss
|
7
|
+
keyup@window->scrim#escape
|
8
|
+
scrim:request:hide@window->scrim#hide
|
9
|
+
scrim:request:show@window->scrim#show
|
10
|
+
ACTIONS
|
11
|
+
end
|
12
|
+
end
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Kpop
|
7
|
+
class Engine < ::Rails::Engine
|
8
|
+
config.autoload_once_paths = %W(#{root}/app/helpers)
|
9
|
+
|
10
|
+
initializer "kpop.helpers", before: :load_config_initializers do
|
11
|
+
ActiveSupport.on_load(:action_controller_base) do
|
12
|
+
helper Katalyst::Kpop::Engine.helpers
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load(:action_view_base) do
|
16
|
+
helper Katalyst::Kpop::Engine.helpers
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer "kpop.assets" do
|
21
|
+
config.after_initialize do |app|
|
22
|
+
if app.config.respond_to?(:assets)
|
23
|
+
app.config.assets.precompile += %w(kpop.js)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
initializer "kpop.importmap", before: "importmap" do |app|
|
29
|
+
if app.config.respond_to?(:importmap)
|
30
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
31
|
+
app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/tasks/yarn.rake
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :yarn do
|
4
|
+
desc "Install npm packages with yarn"
|
5
|
+
task install: :environment do
|
6
|
+
sh "yarn install"
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Lint JS/SCSS files using yarn (prettier)"
|
10
|
+
task lint: :install do
|
11
|
+
sh "yarn lint"
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Autoformat JS/SCSS files using yarn (prettier)"
|
15
|
+
task format: :install do
|
16
|
+
sh "yarn format"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Compile js/css"
|
20
|
+
task build: :install do
|
21
|
+
sh "yarn build && yarn build:css"
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: katalyst-kpop
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Katalyst Interactive
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- developers@katalyst.com.au
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- app/assets/builds/katalyst/kpop.css
|
22
|
+
- app/assets/builds/katalyst/kpop.min.js
|
23
|
+
- app/assets/config/kpop.js
|
24
|
+
- app/assets/javascripts/controllers/kpop_controller.js
|
25
|
+
- app/assets/javascripts/controllers/scrim_controller.js
|
26
|
+
- app/assets/javascripts/katalyst/kpop.js
|
27
|
+
- app/assets/stylesheets/katalyst/kpop.scss
|
28
|
+
- app/assets/stylesheets/katalyst/kpop/_index.scss
|
29
|
+
- app/assets/stylesheets/katalyst/kpop/_kpop.scss
|
30
|
+
- app/assets/stylesheets/katalyst/kpop/_scrim.scss
|
31
|
+
- app/helpers/kpop/modal.rb
|
32
|
+
- app/helpers/kpop_helper.rb
|
33
|
+
- app/helpers/scrim_helper.rb
|
34
|
+
- app/views/layouts/kpop.html.erb
|
35
|
+
- config/importmap.rb
|
36
|
+
- config/routes.rb
|
37
|
+
- lib/katalyst/kpop.rb
|
38
|
+
- lib/katalyst/kpop/engine.rb
|
39
|
+
- lib/katalyst/kpop/version.rb
|
40
|
+
- lib/tasks/yarn.rake
|
41
|
+
homepage: https://github.com/katalyst/kpop
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata:
|
45
|
+
rubygems_mfa_required: 'true'
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.4.10
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Modal library that uses Turbo and Stimulus.
|
65
|
+
test_files: []
|