@c4h/chuci 0.1.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.
@@ -0,0 +1,291 @@
1
+ import { ChuciElement } from '@/utils/base-element'
2
+ import './cc-viewer-image'
3
+ import './cc-viewer-panorama'
4
+ import './cc-viewer-youtube'
5
+ import './cc-viewer-video'
6
+ import './cc-viewer-3dmodel'
7
+ import './cc-viewer-gaussian'
8
+
9
+ interface HashKeyTag {
10
+ [index: string]: string
11
+ }
12
+
13
+ const typeHashes = {
14
+ image: 'cc-viewer-image',
15
+ panorama: 'cc-viewer-panorama',
16
+ youtube: 'cc-viewer-youtube',
17
+ video: 'cc-viewer-video',
18
+ '3dmodel': 'cc-viewer-3dmodel',
19
+ gaussian: 'cc-viewer-gaussian'
20
+ } as HashKeyTag
21
+
22
+ export class CcViewer extends ChuciElement {
23
+ // Swiper instance from cc-swiper component, no TypeScript types available
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ private swiper: any
26
+ private currentSlideIndex = 0
27
+ private currentType = ''
28
+ private boundHandleNavigatePrev?: (e: Event) => void
29
+ private boundHandleNavigateNext?: (e: Event) => void
30
+
31
+
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ open(imgUrl: string, type: string, attributes?: Record<string, any>) {
34
+ const previousType = this.currentType
35
+ this.currentType = type
36
+
37
+ // Close previous viewer if type changed
38
+ if (previousType && previousType !== type) {
39
+ const previousTag = typeHashes[previousType]
40
+ const previousHandler = this.query(previousTag)
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ if (previousHandler && (previousHandler as any).close) {
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ (previousHandler as any).close()
45
+ }
46
+ }
47
+
48
+ // Re-render if type changed
49
+ if (previousType !== type) {
50
+ this.render()
51
+ }
52
+
53
+ // Wait for render to complete
54
+ setTimeout(() => {
55
+ const targetTag = typeHashes[type]
56
+ const handler = this.query(targetTag)
57
+
58
+ // Pass attributes to the viewer if available
59
+ if (handler && attributes) {
60
+ Object.entries(attributes).forEach(([key, value]) => {
61
+ // Convert camelCase to kebab-case for attributes
62
+ const attrName = key.replace(/([A-Z])/g, '-$1').toLowerCase()
63
+ if (typeof value === 'boolean') {
64
+ if (value) {
65
+ handler.setAttribute(attrName, '')
66
+ } else {
67
+ handler.removeAttribute(attrName)
68
+ }
69
+ } else {
70
+ handler.setAttribute(attrName, String(value))
71
+ }
72
+ })
73
+ }
74
+
75
+ // Update navigation button visibility
76
+ this.updateNavigationButtons()
77
+
78
+ if (handler) {
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ ;(handler as any).open(imgUrl)
81
+ }
82
+ }, 0)
83
+ }
84
+
85
+ protected firstUpdated() {
86
+ // Dispatch load event when the component is ready
87
+ this.dispatch('load')
88
+
89
+ // Remove existing listeners if any
90
+ if (this.boundHandleNavigatePrev) {
91
+ this.removeEventListener('navigate-prev', this.boundHandleNavigatePrev)
92
+ }
93
+ if (this.boundHandleNavigateNext) {
94
+ this.removeEventListener('navigate-next', this.boundHandleNavigateNext)
95
+ }
96
+
97
+ // Create bound functions once
98
+ this.boundHandleNavigatePrev = this.handleNavigatePrev.bind(this)
99
+ this.boundHandleNavigateNext = this.handleNavigateNext.bind(this)
100
+
101
+ // Listen for navigation events
102
+ this.addEventListener('navigate-prev', this.boundHandleNavigatePrev)
103
+ this.addEventListener('navigate-next', this.boundHandleNavigateNext)
104
+ }
105
+
106
+ private handleNavigatePrev(_e?: Event) {
107
+ if (!this.swiper) return
108
+
109
+ const totalSlides = this.swiper.slides.length
110
+ if (totalSlides <= 1) return // No navigation for single slide
111
+
112
+ // Check if loop is enabled in swiper
113
+ const hasLoop = this.swiper.slider?.params?.loop === true
114
+
115
+ if (this.currentSlideIndex <= 0) {
116
+ if (hasLoop) {
117
+ this.currentSlideIndex = totalSlides - 1 // Go to last slide
118
+ } else {
119
+ return // Can't go previous
120
+ }
121
+ } else {
122
+ this.currentSlideIndex--
123
+ }
124
+
125
+ this.navigateToSlide(this.currentSlideIndex)
126
+ }
127
+
128
+ private handleNavigateNext(_e?: Event) {
129
+ if (!this.swiper) return
130
+
131
+ const totalSlides = this.swiper.slides.length
132
+ if (totalSlides <= 1) return // No navigation for single slide
133
+
134
+ // Check if loop is enabled in swiper
135
+ const hasLoop = this.swiper.slider?.params?.loop === true
136
+
137
+ if (this.currentSlideIndex >= totalSlides - 1) {
138
+ if (hasLoop) {
139
+ this.currentSlideIndex = 0 // Go to first slide
140
+ } else {
141
+ return // Can't go next
142
+ }
143
+ } else {
144
+ this.currentSlideIndex++
145
+ }
146
+
147
+ this.navigateToSlide(this.currentSlideIndex)
148
+ }
149
+
150
+ private navigateToSlide(index: number) {
151
+ if (!this.swiper || !this.swiper.slides[index]) return
152
+
153
+ // Get new slide info first
154
+ const slide = this.swiper.slides[index]
155
+ const imageUrl = slide.getAttribute('image-url') || ''
156
+ const imageType = slide.getAttribute('image-type') || 'image'
157
+
158
+ // Close current viewer
159
+ const currentTag = typeHashes[this.currentType]
160
+ const currentHandler = this.query(currentTag)
161
+ if (currentHandler) {
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ (currentHandler as any).close()
164
+ }
165
+
166
+ // Gather viewer-specific attributes
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ const attributes: Record<string, any> = {}
169
+ if (slide.hasAttribute('fit-to-container')) {
170
+ attributes.fitToContainer = true
171
+ }
172
+ if (slide.hasAttribute('debug-mode')) {
173
+ attributes.debugMode = true
174
+ }
175
+ if (slide.hasAttribute('camera-position')) {
176
+ attributes.cameraPosition = slide.getAttribute('camera-position')
177
+ }
178
+ if (slide.hasAttribute('camera-target')) {
179
+ attributes.cameraTarget = slide.getAttribute('camera-target')
180
+ }
181
+ if (slide.hasAttribute('show-texture')) {
182
+ attributes.showTexture = slide.getAttribute('show-texture') === 'true'
183
+ }
184
+ if (slide.hasAttribute('material-url')) {
185
+ attributes.materialUrl = slide.getAttribute('material-url')
186
+ }
187
+
188
+ this.currentSlideIndex = index
189
+ this.open(imageUrl, imageType, attributes)
190
+
191
+ // Update swiper position
192
+ if (this.swiper.slider) {
193
+ this.swiper.slider.slideTo(index)
194
+ }
195
+ }
196
+
197
+ private updateNavigationButtons() {
198
+ if (!this.swiper) return
199
+
200
+ const totalSlides = this.swiper.slides.length
201
+ const hasLoop = this.swiper.slider?.params?.loop === true
202
+
203
+ // For single slide, hide both buttons
204
+ if (totalSlides <= 1) {
205
+ this.setNavigationVisibility(false, false)
206
+ return
207
+ }
208
+
209
+ // For multiple slides with loop, show both
210
+ if (hasLoop) {
211
+ this.setNavigationVisibility(true, true)
212
+ return
213
+ }
214
+
215
+ // For multiple slides without loop, check boundaries
216
+ const showPrev = this.currentSlideIndex > 0
217
+ const showNext = this.currentSlideIndex < totalSlides - 1
218
+ this.setNavigationVisibility(showPrev, showNext)
219
+ }
220
+
221
+ private setNavigationVisibility(showPrev: boolean, showNext: boolean) {
222
+ // Update all viewer instances
223
+ const viewers = [
224
+ this.query('cc-viewer-image'),
225
+ this.query('cc-viewer-youtube'),
226
+ this.query('cc-viewer-panorama'),
227
+ this.query('cc-viewer-video'),
228
+ this.query('cc-viewer-3dmodel'),
229
+ this.query('cc-viewer-gaussian')
230
+ ]
231
+
232
+ viewers.forEach(viewer => {
233
+ if (viewer) {
234
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
+ const v = viewer as any
236
+ v.showPrevButton = showPrev
237
+ v.showNextButton = showNext
238
+ }
239
+ })
240
+ }
241
+
242
+ // Public method to set swiper reference
243
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
+ setSwiper(swiper: any) {
245
+ this.swiper = swiper
246
+ }
247
+
248
+ // Public method to set current slide index
249
+ setCurrentSlideIndex(index: number) {
250
+ this.currentSlideIndex = index
251
+ }
252
+
253
+ protected render() {
254
+ const styles = this.css`
255
+ :host {
256
+ --cc-viewer-z-index: 1000;
257
+ }
258
+
259
+ cc-viewer-panorama, cc-viewer-image, cc-viewer-youtube, cc-viewer-video,
260
+ cc-viewer-3dmodel, cc-viewer-gaussian {
261
+ --cc-viewer-z-index-each: var(--cc-viewer-z-index);
262
+ }
263
+ `
264
+
265
+ // Only render the current viewer type
266
+ let viewerHtml = ''
267
+ if (this.currentType) {
268
+ const tag = typeHashes[this.currentType]
269
+ if (tag) {
270
+ viewerHtml = `<${tag}></${tag}>`
271
+ }
272
+ }
273
+
274
+ const html = `
275
+ ${styles}
276
+ ${viewerHtml}
277
+ `
278
+
279
+ this.updateShadowRoot(html)
280
+ }
281
+ }
282
+
283
+ if (!customElements.get('cc-viewer')) {
284
+ customElements.define('cc-viewer', CcViewer)
285
+ }
286
+
287
+ declare global {
288
+ interface HTMLElementTagNameMap {
289
+ 'cc-viewer': CcViewer
290
+ }
291
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ // Export all components
2
+ export * from './components/swiper/cc-swiper'
3
+ export * from './components/swiper/cc-swiper-slide'
4
+ export * from './components/viewer/cc-viewer'
5
+ export * from './components/viewer/cc-viewer-base'
6
+ export * from './components/viewer/cc-viewer-image'
7
+ export * from './components/viewer/cc-viewer-panorama'
8
+ export * from './components/viewer/cc-viewer-youtube'
9
+ export * from './components/viewer/cc-viewer-video'
10
+ export * from './components/viewer/cc-viewer-3dmodel'
11
+ export * from './components/viewer/cc-viewer-gaussian'
12
+
13
+ // Export utilities
14
+ export * from './utils/base-element'
15
+
16
+ // Auto-register components when imported
17
+ import './components/swiper/cc-swiper'
18
+ import './components/swiper/cc-swiper-slide'
19
+ import './components/viewer/cc-viewer'
20
+ import './components/viewer/cc-viewer-image'
21
+ import './components/viewer/cc-viewer-panorama'
22
+ import './components/viewer/cc-viewer-youtube'
23
+ import './components/viewer/cc-viewer-video'
24
+ import './components/viewer/cc-viewer-3dmodel'
25
+ import './components/viewer/cc-viewer-gaussian'
@@ -0,0 +1 @@
1
+ declare module "*.css?inline" { const content: string; export default content; }
@@ -0,0 +1,11 @@
1
+ // Global type declarations for Chuci
2
+
3
+ declare global {
4
+ interface Window {
5
+ // gsplat.js library global - external library without TypeScript types
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ gsplat?: any;
8
+ }
9
+ }
10
+
11
+ export {}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Base class for Chuci web components without Lit dependency
3
+ */
4
+ export abstract class ChuciElement extends HTMLElement {
5
+ private _shadowRoot: ShadowRoot
6
+ private _connected = false
7
+
8
+ constructor() {
9
+ super()
10
+ this._shadowRoot = this.attachShadow({ mode: 'open' })
11
+ }
12
+
13
+ connectedCallback() {
14
+ if (!this._connected) {
15
+ this._connected = true
16
+ this.render()
17
+ this.firstUpdated()
18
+ } else {
19
+ this.render()
20
+ }
21
+ }
22
+
23
+ disconnectedCallback() {
24
+ this._connected = false
25
+ }
26
+
27
+ attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null) {
28
+ if (oldValue !== newValue) {
29
+ this.render()
30
+ }
31
+ }
32
+
33
+ protected firstUpdated(): void {
34
+ // Override in subclasses for initialization logic
35
+ }
36
+
37
+ protected abstract render(): void
38
+
39
+ // Template literal values can be any type that can be converted to string
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ protected html(strings: TemplateStringsArray, ...values: any[]): string {
42
+ return strings.reduce((result, str, i) => {
43
+ return result + str + (values[i] || '')
44
+ }, '')
45
+ }
46
+
47
+ // Template literal values can be any type that can be converted to string
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ protected css(strings: TemplateStringsArray, ...values: any[]): string {
50
+ const cssText = strings.reduce((result, str, i) => {
51
+ return result + str + (values[i] || '')
52
+ }, '')
53
+ return `<style>${cssText}</style>`
54
+ }
55
+
56
+ protected updateShadowRoot(content: string) {
57
+ this._shadowRoot.innerHTML = content
58
+ }
59
+
60
+ protected query<T extends HTMLElement>(selector: string): T | null {
61
+ return this._shadowRoot.querySelector<T>(selector)
62
+ }
63
+
64
+ protected queryAll<T extends HTMLElement>(selector: string): NodeListOf<T> {
65
+ return this._shadowRoot.querySelectorAll<T>(selector)
66
+ }
67
+
68
+ // Custom event detail can be any type
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ protected dispatch(eventName: string, detail?: any) {
71
+ this.dispatchEvent(new CustomEvent(eventName, {
72
+ detail,
73
+ bubbles: true,
74
+ composed: true
75
+ }))
76
+ }
77
+ }