rook-cli 1.3.8 → 1.3.10

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.
@@ -276,6 +276,19 @@
276
276
  --rk-icon-size: 32px;
277
277
  }
278
278
 
279
+ .rk-icon__img {
280
+ display: block;
281
+ width: 100%;
282
+ height: 100%;
283
+ object-fit: contain;
284
+ }
285
+
286
+ .rk-icon-block__link {
287
+ display: inline-flex;
288
+ color: inherit;
289
+ text-decoration: none;
290
+ }
291
+
279
292
  /* --------------------------------------------------------------------------
280
293
  rk-input (Input)
281
294
  -------------------------------------------------------------------------- */
@@ -705,7 +718,7 @@
705
718
  }
706
719
 
707
720
  /* --------------------------------------------------------------------------
708
- rk-modal (Modal)
721
+ rk-modal (Media Lightbox)
709
722
  -------------------------------------------------------------------------- */
710
723
  .rk-modal {
711
724
  position: fixed;
@@ -729,42 +742,235 @@
729
742
  .rk-modal__overlay {
730
743
  position: absolute;
731
744
  inset: 0;
732
- background: rgba(0, 0, 0, 0.5);
745
+ background: rgba(0, 0, 0, 0.85);
733
746
  backdrop-filter: blur(4px);
734
747
  }
735
748
 
736
749
  .rk-modal__content {
737
750
  position: relative;
738
751
  z-index: 1;
739
- background: #fff;
740
- border-radius: var(--rk-radius);
752
+ display: flex;
753
+ align-items: center;
754
+ justify-content: center;
741
755
  max-width: 90vw;
742
756
  max-height: 90vh;
743
- overflow-y: auto;
744
- padding: var(--rk-space-lg);
745
- transform: translateY(16px);
757
+ transform: scale(0.95);
746
758
  transition: transform var(--rk-transition);
747
759
  }
748
760
 
749
761
  .rk-modal--active .rk-modal__content {
750
- transform: translateY(0);
762
+ transform: scale(1);
751
763
  }
752
764
 
753
765
  .rk-modal__close {
754
- position: absolute;
755
- top: var(--rk-space-sm);
756
- right: var(--rk-space-sm);
757
- background: none;
766
+ position: fixed;
767
+ top: var(--rk-space-md);
768
+ right: var(--rk-space-md);
769
+ z-index: 2;
770
+ background: rgba(0, 0, 0, 0.5);
758
771
  border: none;
759
772
  cursor: pointer;
760
- color: var(--rk-color-text-muted);
761
- padding: var(--rk-space-2xs);
773
+ color: #fff;
774
+ padding: var(--rk-space-xs);
762
775
  border-radius: 50%;
763
776
  transition: background-color var(--rk-transition);
777
+ line-height: 0;
764
778
  }
765
779
 
766
780
  .rk-modal__close:hover {
767
- background-color: var(--rk-color-secondary-hover);
781
+ background-color: rgba(255, 255, 255, 0.2);
782
+ }
783
+
784
+ /* Media containers */
785
+ .rk-modal__media {
786
+ display: flex;
787
+ align-items: center;
788
+ justify-content: center;
789
+ }
790
+
791
+ .rk-modal__media--image {
792
+ max-width: 90vw;
793
+ max-height: 90vh;
794
+ }
795
+
796
+ .rk-modal__img {
797
+ display: block;
798
+ max-width: 90vw;
799
+ max-height: 90vh;
800
+ width: auto;
801
+ height: auto;
802
+ object-fit: contain;
803
+ border-radius: var(--rk-radius-sm, 4px);
804
+ }
805
+
806
+ .rk-modal__media--video,
807
+ .rk-modal__media--embed {
808
+ position: relative;
809
+ width: min(90vw, 1200px);
810
+ max-height: 90vh;
811
+ border-radius: var(--rk-radius-sm, 4px);
812
+ overflow: hidden;
813
+ background: #000;
814
+ }
815
+
816
+ .rk-modal__video {
817
+ display: block;
818
+ width: 100%;
819
+ height: 100%;
820
+ object-fit: contain;
821
+ }
822
+
823
+ .rk-modal__iframe {
824
+ display: block;
825
+ width: 100%;
826
+ height: 100%;
827
+ border: none;
828
+ }
829
+
830
+ /* Tap overlay — click to play/pause */
831
+ .rk-modal__tap-overlay {
832
+ position: absolute;
833
+ inset: 0 0 48px 0;
834
+ z-index: 1;
835
+ cursor: pointer;
836
+ }
837
+
838
+ /* Custom controls — Instagram style */
839
+ .rk-modal__controls {
840
+ position: absolute;
841
+ bottom: 0;
842
+ left: 0;
843
+ right: 0;
844
+ z-index: 2;
845
+ display: flex;
846
+ align-items: center;
847
+ gap: 10px;
848
+ padding: 10px 14px;
849
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
850
+ color: #fff;
851
+ opacity: 1;
852
+ transition: opacity 0.25s ease;
853
+ }
854
+
855
+ .rk-modal__media--video:hover .rk-modal__controls,
856
+ .rk-modal__controls:focus-within {
857
+ opacity: 1;
858
+ }
859
+
860
+ /* Control buttons */
861
+ .rk-modal__ctrl-btn {
862
+ display: flex;
863
+ align-items: center;
864
+ justify-content: center;
865
+ width: 32px;
866
+ height: 32px;
867
+ flex-shrink: 0;
868
+ padding: 0;
869
+ background: none;
870
+ border: none;
871
+ color: #fff;
872
+ cursor: pointer;
873
+ border-radius: 50%;
874
+ transition: background-color 0.15s ease;
875
+ }
876
+
877
+ .rk-modal__ctrl-btn:hover {
878
+ background-color: rgba(255, 255, 255, 0.15);
879
+ }
880
+
881
+ .rk-modal__ctrl-icon {
882
+ width: 18px;
883
+ height: 18px;
884
+ display: none;
885
+ }
886
+
887
+ /* Play/pause icon toggle */
888
+ .rk-modal__controls.is-paused .rk-modal__ctrl-icon--play,
889
+ .rk-modal__controls:not(.is-playing):not(.is-paused) .rk-modal__ctrl-icon--play {
890
+ display: block;
891
+ }
892
+
893
+ .rk-modal__controls.is-playing .rk-modal__ctrl-icon--pause {
894
+ display: block;
895
+ }
896
+
897
+ /* Mute icon toggle */
898
+ .rk-modal__controls.is-muted .rk-modal__ctrl-icon--muted {
899
+ display: block;
900
+ }
901
+
902
+ .rk-modal__controls.is-unmuted .rk-modal__ctrl-icon--unmuted {
903
+ display: block;
904
+ }
905
+
906
+ .rk-modal__controls:not(.is-muted):not(.is-unmuted) .rk-modal__ctrl-icon--muted {
907
+ display: block;
908
+ }
909
+
910
+ /* Progress bar */
911
+ .rk-modal__progress {
912
+ flex: 1;
913
+ height: 3px;
914
+ background: rgba(255, 255, 255, 0.3);
915
+ border-radius: 2px;
916
+ cursor: pointer;
917
+ position: relative;
918
+ transition: height 0.15s ease;
919
+ }
920
+
921
+ .rk-modal__progress:hover {
922
+ height: 5px;
923
+ }
924
+
925
+ .rk-modal__progress-bar {
926
+ height: 100%;
927
+ width: 0%;
928
+ background: #fff;
929
+ border-radius: 2px;
930
+ transition: none;
931
+ pointer-events: none;
932
+ }
933
+
934
+ /* Time display */
935
+ .rk-modal__time {
936
+ font-size: 12px;
937
+ font-variant-numeric: tabular-nums;
938
+ min-width: 32px;
939
+ text-align: center;
940
+ opacity: 0.85;
941
+ user-select: none;
942
+ }
943
+
944
+ .rk-modal__media--html {
945
+ background: #fff;
946
+ border-radius: var(--rk-radius);
947
+ padding: var(--rk-space-lg);
948
+ max-width: min(90vw, 800px);
949
+ max-height: 90vh;
950
+ overflow-y: auto;
951
+ }
952
+
953
+ @media (max-width: 749px) {
954
+ .rk-modal__media--video,
955
+ .rk-modal__media--embed {
956
+ width: 100vw;
957
+ border-radius: 0;
958
+ }
959
+
960
+ .rk-modal__controls {
961
+ padding: 8px 10px;
962
+ gap: 8px;
963
+ }
964
+
965
+ .rk-modal__ctrl-btn {
966
+ width: 28px;
967
+ height: 28px;
968
+ }
969
+
970
+ .rk-modal__ctrl-icon {
971
+ width: 16px;
972
+ height: 16px;
973
+ }
768
974
  }
769
975
 
770
976
  /* --------------------------------------------------------------------------
@@ -2538,6 +2744,17 @@ a.rk-card {
2538
2744
  height: 32px;
2539
2745
  }
2540
2746
 
2747
+ /* --------------------------------------------------------------------------
2748
+ rk-reveal (Scroll Reveal Animation)
2749
+ -------------------------------------------------------------------------- */
2750
+ .rk-reveal {
2751
+ display: block;
2752
+ }
2753
+
2754
+ .rk-reveal__content {
2755
+ will-change: opacity, transform;
2756
+ }
2757
+
2541
2758
  /* --------------------------------------------------------------------------
2542
2759
  rk-sr-only (Screen Reader Only utility)
2543
2760
  -------------------------------------------------------------------------- */
@@ -1,6 +1,9 @@
1
1
  /* ==========================================================================
2
- Rook UI Core — Modal Controller
2
+ Rook UI Core — Modal Media Lightbox Controller
3
3
  Web Component: <rk-modal-element>
4
+
5
+ Handles image zoom, hosted video (with custom Instagram-style controls),
6
+ YouTube/Vimeo embeds, and raw HTML in a fullscreen lightbox overlay.
4
7
  ========================================================================== */
5
8
 
6
9
  if (!customElements.get('rk-modal-element')) {
@@ -8,33 +11,39 @@ if (!customElements.get('rk-modal-element')) {
8
11
  constructor() {
9
12
  super();
10
13
  this.modal = null;
11
- this.focusableElements = [];
14
+ this.content = null;
12
15
  this.previousFocus = null;
16
+ this._raf = null;
17
+
18
+ this._onKeyDown = this._onKeyDown.bind(this);
13
19
  }
14
20
 
15
21
  connectedCallback() {
16
22
  this.modal = this.querySelector('.rk-modal');
23
+ this.content = this.querySelector('.rk-modal__content');
24
+
17
25
  if (!this.modal) return;
18
26
 
19
- this.bindEvents();
20
- this.setupOpenTriggers();
27
+ this._bindEvents();
28
+ this._setupOpenTriggers();
29
+ this._setupVideoControls();
21
30
  }
22
31
 
23
- bindEvents() {
24
- // Close buttons (overlay & X)
32
+ disconnectedCallback() {
33
+ if (this._raf) cancelAnimationFrame(this._raf);
34
+ }
35
+
36
+ /* ------------------------------------------------------------------ */
37
+ /* Events */
38
+ /* ------------------------------------------------------------------ */
39
+
40
+ _bindEvents() {
25
41
  this.querySelectorAll('[data-action="close"]').forEach((el) => {
26
42
  el.addEventListener('click', () => this.close());
27
43
  });
28
-
29
- // ESC key
30
- document.addEventListener('keydown', (e) => {
31
- if (e.key === 'Escape' && this.isOpen()) {
32
- this.close();
33
- }
34
- });
35
44
  }
36
45
 
37
- setupOpenTriggers() {
46
+ _setupOpenTriggers() {
38
47
  const modalId = this.dataset.modalId || this.modal?.id;
39
48
  if (!modalId) return;
40
49
 
@@ -46,45 +55,262 @@ if (!customElements.get('rk-modal-element')) {
46
55
  });
47
56
  }
48
57
 
58
+ /* ------------------------------------------------------------------ */
59
+ /* Custom video controls (Instagram-style) */
60
+ /* ------------------------------------------------------------------ */
61
+
62
+ _setupVideoControls() {
63
+ const video = this.querySelector('.rk-modal__video');
64
+ if (!video) return;
65
+
66
+ this._video = video;
67
+ this._controls = this.querySelector('.rk-modal__controls');
68
+ this._progressBar = this.querySelector('.rk-modal__progress-bar');
69
+ this._progressTrack = this.querySelector('[data-ctrl="progress"]');
70
+ this._timeDisplay = this.querySelector('[data-ctrl="time"]');
71
+ this._playPauseBtn = this.querySelector('[data-ctrl="play-pause"]');
72
+ this._muteBtn = this.querySelector('[data-ctrl="mute"]');
73
+ this._tapOverlay = this.querySelector('[data-ctrl="tap"]');
74
+
75
+ // Play/pause button
76
+ if (this._playPauseBtn) {
77
+ this._playPauseBtn.addEventListener('click', (e) => {
78
+ e.stopPropagation();
79
+ this._togglePlay();
80
+ });
81
+ }
82
+
83
+ // Mute button
84
+ if (this._muteBtn) {
85
+ this._muteBtn.addEventListener('click', (e) => {
86
+ e.stopPropagation();
87
+ this._toggleMute();
88
+ });
89
+ }
90
+
91
+ // Tap overlay — toggle play/pause
92
+ if (this._tapOverlay) {
93
+ this._tapOverlay.addEventListener('click', (e) => {
94
+ e.stopPropagation();
95
+ this._togglePlay();
96
+ });
97
+ }
98
+
99
+ // Progress bar click — seek
100
+ if (this._progressTrack) {
101
+ this._progressTrack.addEventListener('click', (e) => {
102
+ e.stopPropagation();
103
+ this._seek(e);
104
+ });
105
+ }
106
+
107
+ // Sync UI with video state
108
+ video.addEventListener('play', () => this._syncPlayState());
109
+ video.addEventListener('pause', () => this._syncPlayState());
110
+ video.addEventListener('volumechange', () => this._syncMuteState());
111
+ }
112
+
113
+ _togglePlay() {
114
+ if (!this._video) return;
115
+ if (this._video.paused) {
116
+ this._video.play().catch(() => {});
117
+ } else {
118
+ this._video.pause();
119
+ }
120
+ }
121
+
122
+ _toggleMute() {
123
+ if (!this._video) return;
124
+ this._video.muted = !this._video.muted;
125
+ }
126
+
127
+ _seek(e) {
128
+ if (!this._video || !this._progressTrack) return;
129
+ const rect = this._progressTrack.getBoundingClientRect();
130
+ const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
131
+ this._video.currentTime = ratio * this._video.duration;
132
+ }
133
+
134
+ _syncPlayState() {
135
+ if (!this._video || !this._controls) return;
136
+ const playing = !this._video.paused;
137
+ this._controls.classList.toggle('is-playing', playing);
138
+ this._controls.classList.toggle('is-paused', !playing);
139
+
140
+ if (playing) {
141
+ this._startProgressLoop();
142
+ } else {
143
+ this._stopProgressLoop();
144
+ }
145
+ }
146
+
147
+ _syncMuteState() {
148
+ if (!this._video || !this._controls) return;
149
+ this._controls.classList.toggle('is-muted', this._video.muted);
150
+ this._controls.classList.toggle('is-unmuted', !this._video.muted);
151
+ }
152
+
153
+ _startProgressLoop() {
154
+ const update = () => {
155
+ if (!this._video || this._video.paused) return;
156
+
157
+ const pct = (this._video.currentTime / this._video.duration) * 100 || 0;
158
+ if (this._progressBar) {
159
+ this._progressBar.style.width = pct + '%';
160
+ }
161
+
162
+ if (this._timeDisplay) {
163
+ this._timeDisplay.textContent = this._formatTime(this._video.currentTime);
164
+ }
165
+
166
+ this._raf = requestAnimationFrame(update);
167
+ };
168
+ this._raf = requestAnimationFrame(update);
169
+ }
170
+
171
+ _stopProgressLoop() {
172
+ if (this._raf) {
173
+ cancelAnimationFrame(this._raf);
174
+ this._raf = null;
175
+ }
176
+ }
177
+
178
+ _formatTime(seconds) {
179
+ const m = Math.floor(seconds / 60);
180
+ const s = Math.floor(seconds % 60);
181
+ return m + ':' + (s < 10 ? '0' : '') + s;
182
+ }
183
+
184
+ /* ------------------------------------------------------------------ */
185
+ /* Public API */
186
+ /* ------------------------------------------------------------------ */
187
+
49
188
  isOpen() {
50
189
  return this.modal?.classList.contains('rk-modal--active');
51
190
  }
52
191
 
53
192
  open() {
54
- if (!this.modal) return;
193
+ if (!this.modal || this.isOpen()) return;
55
194
 
56
195
  this.previousFocus = document.activeElement;
57
196
  this.modal.classList.add('rk-modal--active');
58
197
  document.body.style.overflow = 'hidden';
198
+ document.addEventListener('keydown', this._onKeyDown);
59
199
 
60
- // Trap focus
61
- this.focusableElements = this.modal.querySelectorAll(
62
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
63
- );
200
+ // Load lazy iframes (YouTube/Vimeo)
201
+ this._loadIframes();
202
+
203
+ // Autoplay hosted video (muted)
204
+ this._playMedia();
64
205
 
65
- if (this.focusableElements.length > 0) {
66
- this.focusableElements[0].focus();
206
+ // Sync initial state
207
+ if (this._video) {
208
+ this._syncPlayState();
209
+ this._syncMuteState();
67
210
  }
68
211
 
212
+ // Focus close button
213
+ requestAnimationFrame(() => {
214
+ const closeBtn = this.querySelector('.rk-modal__close');
215
+ if (closeBtn) closeBtn.focus();
216
+ });
217
+
69
218
  this.dispatchEvent(
70
- new CustomEvent('rk:modal:open', { bubbles: true, detail: { id: this.modal.id } })
219
+ new CustomEvent('rk:modal:open', {
220
+ bubbles: true,
221
+ detail: { id: this.modal.id, type: this.dataset.type },
222
+ })
71
223
  );
72
224
  }
73
225
 
74
226
  close() {
75
- if (!this.modal) return;
227
+ if (!this.modal || !this.isOpen()) return;
76
228
 
77
229
  this.modal.classList.remove('rk-modal--active');
78
230
  document.body.style.overflow = '';
231
+ document.removeEventListener('keydown', this._onKeyDown);
232
+
233
+ // Stop all media
234
+ this._stopMedia();
235
+ this._stopProgressLoop();
79
236
 
80
237
  if (this.previousFocus) {
81
238
  this.previousFocus.focus();
239
+ this.previousFocus = null;
82
240
  }
83
241
 
84
242
  this.dispatchEvent(
85
- new CustomEvent('rk:modal:close', { bubbles: true, detail: { id: this.modal.id } })
243
+ new CustomEvent('rk:modal:close', {
244
+ bubbles: true,
245
+ detail: { id: this.modal.id, type: this.dataset.type },
246
+ })
86
247
  );
87
248
  }
249
+
250
+ /* ------------------------------------------------------------------ */
251
+ /* Media helpers */
252
+ /* ------------------------------------------------------------------ */
253
+
254
+ _loadIframes() {
255
+ this.querySelectorAll('iframe[data-src]').forEach((iframe) => {
256
+ if (!iframe.src || iframe.src === 'about:blank') {
257
+ iframe.src = iframe.dataset.src;
258
+ }
259
+ });
260
+ }
261
+
262
+ _playMedia() {
263
+ const video = this.querySelector('video[data-autoplay]');
264
+ if (video) {
265
+ video.muted = true;
266
+ video.currentTime = 0;
267
+ video.play().catch(() => {
268
+ /* autoplay blocked — silent fail */
269
+ });
270
+ }
271
+ }
272
+
273
+ _stopMedia() {
274
+ // Pause hosted videos
275
+ this.querySelectorAll('video').forEach((v) => {
276
+ v.pause();
277
+ v.currentTime = 0;
278
+ });
279
+
280
+ // Clear iframe src to stop YouTube/Vimeo playback
281
+ this.querySelectorAll('iframe').forEach((iframe) => {
282
+ if (iframe.dataset.src) {
283
+ iframe.src = 'about:blank';
284
+ }
285
+ });
286
+ }
287
+
288
+ /* ------------------------------------------------------------------ */
289
+ /* Keyboard (ESC + Space for play/pause) */
290
+ /* ------------------------------------------------------------------ */
291
+
292
+ _onKeyDown(e) {
293
+ if (!this.isOpen()) return;
294
+
295
+ if (e.key === 'Escape') {
296
+ e.preventDefault();
297
+ this.close();
298
+ return;
299
+ }
300
+
301
+ // Space toggles play/pause on video
302
+ if (e.key === ' ' && this._video) {
303
+ e.preventDefault();
304
+ this._togglePlay();
305
+ return;
306
+ }
307
+
308
+ // M toggles mute on video
309
+ if (e.key === 'm' && this._video) {
310
+ e.preventDefault();
311
+ this._toggleMute();
312
+ }
313
+ }
88
314
  }
89
315
 
90
316
  customElements.define('rk-modal-element', RkModalElement);