js-kompo-utils 1.0.17 → 1.0.18

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,799 @@
1
+ <template>
2
+ <component :is="tag" v-bind="rootAttrs" :class="rootClasses" ref="root" @keyup.esc="closeDropdown">
3
+ <slot v-bind="slotProps">
4
+ <button v-if="isDropdown" type="button" class="lazy-hierarchy__trigger" :class="uiClasses.trigger" :aria-label="triggerLabel || labels.trigger" @click="togglePanel">
5
+ <span class="lazy-hierarchy__trigger-label">{{ triggerLabel }}</span>
6
+ <svg class="lazy-hierarchy__trigger-icon" :class="{ 'lazy-hierarchy__trigger-icon--open': open }" viewBox="0 0 20 20" aria-hidden="true">
7
+ <path d="M5 7.5l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
8
+ </svg>
9
+ </button>
10
+
11
+ <div v-if="panelOpen" class="lazy-hierarchy__panel" :class="uiClasses.panel">
12
+ <div v-if="hasControls" class="lazy-hierarchy__top">
13
+ <div v-if="searchEnabled" class="vlInputWrapper lazy-hierarchy__search-wrap" :class="uiClasses.searchWrapper">
14
+ <input v-model="search" class="vlFormControl lazy-hierarchy__search" :class="uiClasses.searchInput" type="search" :placeholder="searchPlaceholder" />
15
+ </div>
16
+
17
+ <div v-if="modeOptionsList.length" class="vlRadio lazy-hierarchy__modes" :class="uiClasses.modes">
18
+ <div
19
+ v-for="modeOption in modeOptionsList"
20
+ :key="modeOption.value"
21
+ class="lazy-hierarchy__mode"
22
+ :class="[uiClasses.mode, { 'lazy-hierarchy__mode--active': mode === modeOption.value }]"
23
+ @click="mode = modeOption.value"
24
+ >
25
+ <div class="vlInputWrapper lazy-hierarchy__radio-control">
26
+ <input v-model="mode" type="radio" :value="modeOption.value" :id="modeRadioId(modeOption.value)" />
27
+ <span class="lazy-hierarchy__radio-dot" aria-hidden="true"></span>
28
+ </div>
29
+ <label class="vlFormLabel lazy-hierarchy__mode-label" :for="modeRadioId(modeOption.value)">{{ modeOption.label }}</label>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="lazy-hierarchy__body mini-scroll" :class="uiClasses.body">
35
+ <div v-if="isTreeLoading && !hasNodes" class="lazy-hierarchy__status">
36
+ <span class="lazy-hierarchy__status-label">
37
+ <span class="lazy-hierarchy__spinner" aria-hidden="true"></span>
38
+ <span>{{ labels.loading }}</span>
39
+ </span>
40
+ <span v-for="index in 5" :key="index" class="lazy-hierarchy__skeleton-row" aria-hidden="true">
41
+ <span class="lazy-hierarchy__skeleton-name"></span>
42
+ <span class="lazy-hierarchy__skeleton-pill"></span>
43
+ </span>
44
+ </div>
45
+ <div v-else-if="error" class="lazy-hierarchy__status lazy-hierarchy__status--error">{{ labels.error }}</div>
46
+ <div v-else-if="bootstrapped && rootNodeIds.length === 0" class="lazy-hierarchy__status">{{ labels.empty }}</div>
47
+
48
+ <vl-lazy-tree v-else v-bind="treeProps" @toggle="toggleNode" @load-more="loadMoreFromTree">
49
+ <template v-slot:node="item">
50
+ <div class="lazy-hierarchy-node" :class="{ 'lazy-hierarchy-node--current': item.node.isCurrent }">
51
+ <div class="lazy-hierarchy-node__row" :class="uiClasses.nodeRow" :style="item.indentStyle">
52
+ <button
53
+ v-if="item.hasChildren"
54
+ type="button"
55
+ class="lazy-hierarchy-node__toggle"
56
+ :aria-expanded="item.isExpanded ? 'true' : 'false'"
57
+ @click.stop="item.toggle"
58
+ >
59
+ <svg class="lazy-hierarchy-node__chevron" :class="{ 'lazy-hierarchy-node__chevron--open': item.isExpanded }" viewBox="0 0 20 20" aria-hidden="true">
60
+ <path d="M7.5 5l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
61
+ </svg>
62
+ </button>
63
+ <span v-else class="lazy-hierarchy-node__toggle-spacer"></span>
64
+
65
+ <component v-bind="kompoElementAttributes(item.node.render, item)" class="lazy-hierarchy-node__content" />
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <template v-slot:loading="item">
71
+ <div :key="item.key" class="lazy-hierarchy-node__loading" :style="item.indentStyle">
72
+ <span class="lazy-hierarchy__skeleton-row" aria-hidden="true">
73
+ <span class="lazy-hierarchy__skeleton-name"></span>
74
+ <span class="lazy-hierarchy__skeleton-pill"></span>
75
+ </span>
76
+ </div>
77
+ </template>
78
+
79
+ <template v-slot:load-more="item">
80
+ <vl-infinite-load
81
+ :key="item.key"
82
+ class="lazy-hierarchy__show-more"
83
+ :button-class="uiClasses.showMore"
84
+ :loading="item.isLoading"
85
+ :disabled="item.isLoading"
86
+ :label="labels.showMore"
87
+ :loading-label="labels.loading"
88
+ @visible="item.loadMore"
89
+ />
90
+ </template>
91
+ </vl-lazy-tree>
92
+ </div>
93
+ </div>
94
+ </slot>
95
+ </component>
96
+ </template>
97
+
98
+ <script>
99
+ import Other from 'vue-kompo/js/form/mixins/Other'
100
+ import VlInfiniteLoad from './VlInfiniteLoad.vue'
101
+ import VlLazyTree from './VlLazyTree.vue'
102
+
103
+ const ROOT_KEY = 'root'
104
+
105
+ const configValue = (vm, key) => vm.vkompo && vm.vkompo.config ? vm.vkompo.config[key] : undefined
106
+
107
+ export default {
108
+ name: 'VlLazyHierarchy',
109
+ inheritAttrs: false,
110
+ mixins: [Other],
111
+ components: { VlInfiniteLoad, VlLazyTree },
112
+ props: {
113
+ vkompo: { type: Object, default: null },
114
+ kompoid: { type: String, default: '' },
115
+ kompoinfo: { type: String, default: '' },
116
+ tag: { type: String, default: 'div' },
117
+ rootKey: { type: String, default: ROOT_KEY },
118
+ parentParam: { type: String, default: 'parent_id' },
119
+ cursorParam: { type: String, default: 'cursor' },
120
+ limit: { type: Number, default: 20 },
121
+ lookahead: { type: Number, default: 80 },
122
+ autoLoad: { type: Boolean, default: true },
123
+ deferLoad: { type: Boolean, default: false },
124
+ baseIndent: { type: Number, default: 1 },
125
+ indentStep: { type: Number, default: 0.9 },
126
+ maxIndentDepth: { type: Number, default: 5 },
127
+ },
128
+ data() {
129
+ const modes = configValue(this, 'modes') || {}
130
+ const modeOptions = modes.options || {}
131
+
132
+ return {
133
+ open: false,
134
+ mode: modes.default || Object.keys(modeOptions)[0] || '',
135
+ search: '',
136
+ appliedSearch: '',
137
+ searchTimer: null,
138
+ nodesById: {},
139
+ childrenByParent: { [this.resolvedRootKey]: [] },
140
+ pagingByParent: {},
141
+ expanded: {},
142
+ loadingParents: {},
143
+ loadingInitial: false,
144
+ bootstrapped: false,
145
+ bootstrapPromise: null,
146
+ error: '',
147
+ requestSeq: 0,
148
+ }
149
+ },
150
+ computed: {
151
+ rootAttrs() {
152
+ return {
153
+ ...this.$attrs,
154
+ ...(this.vkompo && this.vkompo.id ? { id: this.vkompo.id } : {}),
155
+ style: [this.vkompo?.style, this.$attrs.style].filter(Boolean),
156
+ }
157
+ },
158
+ rootClasses() {
159
+ return ['vl-lazy-hierarchy', 'lazy-hierarchy', `lazy-hierarchy--${this.displayMode}`, this.vkompo?.class].filter(Boolean)
160
+ },
161
+ displayMode() { return this.configOr('displayMode', 'inline') },
162
+ isDropdown() { return this.displayMode !== 'inline' },
163
+ panelOpen() { return this.open || !this.isDropdown },
164
+ hasControls() { return this.searchEnabled || this.modeOptionsList.length > 0 },
165
+ triggerLabel() { return this.configValue('triggerLabel') || '' },
166
+ searchConfig() { return this.configOr('search', {}) },
167
+ searchEnabled() { return !!this.searchConfig.enabled },
168
+ searchPlaceholder() { return this.searchConfig.placeholder || this.labels.searchPlaceholder },
169
+ searchParam() { return this.searchConfig.param || 'search' },
170
+ searchDebounce() { return this.searchConfig.debounce || 350 },
171
+ modeConfig() { return this.configOr('modes', {}) },
172
+ modeParam() { return this.modeConfig.param || 'mode' },
173
+ modeOptionsList() {
174
+ return Object.entries(this.modeConfig.options || {}).map(([value, label]) => ({ value, label }))
175
+ },
176
+ labels() {
177
+ const labels = this.configOr('labels', {})
178
+ return {
179
+ searchPlaceholder: labels.searchPlaceholder || 'Search',
180
+ loading: labels.loading || 'Loading...',
181
+ empty: labels.empty || 'No items found',
182
+ error: labels.error || 'Unable to load items',
183
+ showMore: labels.showMore || 'Show more',
184
+ trigger: labels.trigger || 'Open',
185
+ }
186
+ },
187
+ uiClasses() { return this.configOr('classes', {}) },
188
+ resolvedRootKey() { return this.configOr('rootKey', this.rootKey) },
189
+ resolvedBootstrapUrl() { return this.configValue('bootstrapUrl') },
190
+ resolvedNodesUrl() { return this.configValue('nodesUrl') },
191
+ resolvedRequestParams() {
192
+ const params = { ...(this.configValue('params') || {}) }
193
+ if (this.searchEnabled) params[this.searchParam] = this.appliedSearch
194
+ if (this.modeOptionsList.length && this.mode) params[this.modeParam] = this.mode
195
+ return params
196
+ },
197
+ resolvedLimit() { return this.configOr('limit', this.limit) },
198
+ resolvedLookahead() { return this.configOr('lookahead', this.lookahead) },
199
+ resolvedBaseIndent() { return this.configOr('baseIndent', this.baseIndent) },
200
+ resolvedIndentStep() { return this.configOr('indentStep', this.indentStep) },
201
+ resolvedMaxIndentDepth() { return this.configOr('maxIndentDepth', this.maxIndentDepth) },
202
+ resolvedParentParam() { return this.configValue('parentParam') || this.parentParam },
203
+ resolvedCursorParam() { return this.configValue('cursorParam') || this.cursorParam },
204
+ resolvedAutoLoad() {
205
+ const c = this.configValue('autoLoad')
206
+ return c === undefined ? this.autoLoad : !!c
207
+ },
208
+ resolvedDeferLoad() {
209
+ const c = this.configValue('deferLoad')
210
+ return c === undefined ? this.deferLoad : !!c
211
+ },
212
+ rootNodeIds() { return this.childrenByParent[this.resolvedRootKey] || [] },
213
+ hasNodes() { return Object.keys(this.nodesById).length > 0 },
214
+ isTreeLoading() { return this.loadingInitial || !!this.bootstrapPromise },
215
+ treeProps() {
216
+ return {
217
+ rootKey: this.resolvedRootKey,
218
+ rootNodeIds: this.rootNodeIds,
219
+ nodesById: this.nodesById,
220
+ childrenByParent: this.childrenByParent,
221
+ pagingByParent: this.pagingByParent,
222
+ expanded: this.expanded,
223
+ loadingParents: this.loadingParents,
224
+ baseIndent: this.resolvedBaseIndent,
225
+ indentStep: this.resolvedIndentStep,
226
+ maxIndentDepth: this.resolvedMaxIndentDepth,
227
+ loadingLabel: this.labels.loading,
228
+ showMoreLabel: this.labels.showMore,
229
+ }
230
+ },
231
+ slotProps() {
232
+ return {
233
+ ...this.treeProps,
234
+ treeProps: this.treeProps,
235
+ rootNodeIds: this.rootNodeIds,
236
+ hasNodes: this.hasNodes,
237
+ isTreeLoading: this.isTreeLoading,
238
+ bootstrapped: this.bootstrapped,
239
+ error: this.error,
240
+ ensureBootstrap: this.ensureBootstrap,
241
+ reloadTree: this.reloadTree,
242
+ resetTree: this.resetTree,
243
+ toggleNode: this.toggleNode,
244
+ loadChildren: this.loadChildren,
245
+ loadMore: this.loadMore,
246
+ loadMoreRoot: this.loadMoreRoot,
247
+ loadMoreFromTree: this.loadMoreFromTree,
248
+ }
249
+ },
250
+ },
251
+ watch: {
252
+ mode() {
253
+ if (this.searchTimer) {
254
+ clearTimeout(this.searchTimer)
255
+ this.searchTimer = null
256
+ this.appliedSearch = this.search
257
+ }
258
+ if (this.bootstrapped || this.bootstrapPromise) this.reloadTree()
259
+ },
260
+ search() {
261
+ clearTimeout(this.searchTimer)
262
+ this.searchTimer = setTimeout(() => {
263
+ this.appliedSearch = this.search
264
+ if (this.bootstrapped || this.bootstrapPromise) this.reloadTree()
265
+ }, this.searchDebounce)
266
+ },
267
+ },
268
+ mounted() {
269
+ document.addEventListener('click', this.onDocumentClick)
270
+ if (!this.resolvedAutoLoad) return
271
+ if (this.resolvedDeferLoad) { this.deferBootstrap(); return }
272
+ this.ensureBootstrap(true)
273
+ },
274
+ beforeDestroy() {
275
+ document.removeEventListener('click', this.onDocumentClick)
276
+ clearTimeout(this.searchTimer)
277
+ },
278
+ methods: {
279
+ configValue(key) { return configValue(this, key) },
280
+ configOr(key, fallback) {
281
+ const v = this.configValue(key)
282
+ return v === undefined ? fallback : v
283
+ },
284
+ modeRadioId(value) { return `lazy-hierarchy-${this._uid}-${value}` },
285
+ togglePanel() {
286
+ if (!this.isDropdown) return
287
+ this.open = !this.open
288
+ if (this.open) this.ensureBootstrap(true)
289
+ },
290
+ closeDropdown() { if (this.isDropdown) this.open = false },
291
+ onDocumentClick(event) {
292
+ if (!this.isDropdown || !this.open || !this.$refs.root || this.$refs.root.contains(event.target)) return
293
+ this.open = false
294
+ },
295
+ deferBootstrap() {
296
+ const run = () => this.ensureBootstrap(false)
297
+ if (window.requestIdleCallback) { window.requestIdleCallback(run, { timeout: 1200 }); return }
298
+ setTimeout(run, 250)
299
+ },
300
+ reloadTree(showLoading = true) {
301
+ this.resetTree()
302
+ return this.ensureBootstrap(showLoading)
303
+ },
304
+ resetTree() {
305
+ this.nodesById = {}
306
+ this.childrenByParent = { [this.resolvedRootKey]: [] }
307
+ this.pagingByParent = {}
308
+ this.expanded = {}
309
+ this.loadingParents = {}
310
+ this.bootstrapped = false
311
+ this.bootstrapPromise = null
312
+ this.error = ''
313
+ },
314
+ ensureBootstrap(showLoading = true) {
315
+ if (this.bootstrapped) return Promise.resolve()
316
+ if (this.bootstrapPromise) {
317
+ if (showLoading) this.loadingInitial = true
318
+ return this.bootstrapPromise
319
+ }
320
+ const seq = ++this.requestSeq
321
+ if (showLoading) this.loadingInitial = true
322
+ this.error = ''
323
+ this.bootstrapPromise = this.getJson(this.resolvedBootstrapUrl, this.baseParams())
324
+ .then(payload => {
325
+ if (seq !== this.requestSeq) return
326
+ this.mergePayload(payload)
327
+ this.bootstrapped = true
328
+ this.$emit('loaded', payload)
329
+ })
330
+ .catch(error => this.handleError(error, seq))
331
+ .finally(() => {
332
+ if (seq === this.requestSeq) {
333
+ this.loadingInitial = false
334
+ this.bootstrapPromise = null
335
+ }
336
+ })
337
+ return this.bootstrapPromise
338
+ },
339
+ baseParams(extra = {}) {
340
+ return { ...this.resolvedRequestParams, limit: this.resolvedLimit, lookahead: this.resolvedLookahead, ...extra }
341
+ },
342
+ toggleNode(node) {
343
+ if (!node || !node.id || !node.hasChildren) return
344
+ if (this.expanded[node.id]) { this.$set(this.expanded, node.id, false); return }
345
+ this.$set(this.expanded, node.id, true)
346
+ if (!Object.prototype.hasOwnProperty.call(this.childrenByParent, node.id)) this.loadChildren(node)
347
+ },
348
+ loadChildren(node) {
349
+ if (!node || this.loadingParents[node.id]) return Promise.resolve()
350
+ this.$set(this.loadingParents, node.id, true)
351
+ return this.getJson(this.resolvedNodesUrl, this.baseParams({ [this.resolvedParentParam]: node.id }))
352
+ .then(payload => this.mergePayload(payload))
353
+ .catch(error => this.handleError(error))
354
+ .finally(() => this.$set(this.loadingParents, node.id, false))
355
+ },
356
+ loadMore(node) {
357
+ if (!node || !node.id || this.loadingParents[node.id]) return Promise.resolve()
358
+ const paging = this.pagingByParent[node.id]
359
+ if (!paging || paging.nextCursor === null) return Promise.resolve()
360
+ this.$set(this.loadingParents, node.id, true)
361
+ return this.getJson(this.resolvedNodesUrl, this.baseParams({
362
+ [this.resolvedParentParam]: node.id,
363
+ [this.resolvedCursorParam]: paging.nextCursor,
364
+ }))
365
+ .then(payload => this.mergePayload(payload, node.id))
366
+ .catch(error => this.handleError(error))
367
+ .finally(() => this.$set(this.loadingParents, node.id, false))
368
+ },
369
+ loadMoreRoot() {
370
+ const rootKey = this.resolvedRootKey
371
+ const paging = this.pagingByParent[rootKey]
372
+ if (!paging || paging.nextCursor === null || this.loadingParents[rootKey]) return Promise.resolve()
373
+ this.$set(this.loadingParents, rootKey, true)
374
+ return this.getJson(this.resolvedNodesUrl, this.baseParams({ [this.resolvedCursorParam]: paging.nextCursor }))
375
+ .then(payload => this.mergePayload(payload, rootKey))
376
+ .catch(error => this.handleError(error))
377
+ .finally(() => this.$set(this.loadingParents, rootKey, false))
378
+ },
379
+ loadMoreFromTree(item) {
380
+ if (!item) return Promise.resolve()
381
+ return item.root ? this.loadMoreRoot() : this.loadMore(item.parentNode)
382
+ },
383
+ mergePayload(payload, appendParentKey = null) {
384
+ Object.entries(payload.nodes || {}).forEach(([id, node]) => { this.$set(this.nodesById, id, node) })
385
+ Object.entries(payload.children || {}).forEach(([parentKey, childIds]) => {
386
+ const existing = this.childrenByParent[parentKey] || []
387
+ const shouldAppend = appendParentKey === parentKey || (payload.paging && payload.paging[parentKey] && payload.paging[parentKey].append)
388
+ const next = shouldAppend ? this.unique([...existing, ...childIds]) : this.unique(childIds)
389
+ this.$set(this.childrenByParent, parentKey, next)
390
+ })
391
+ Object.entries(payload.paging || {}).forEach(([parentKey, paging]) => { this.$set(this.pagingByParent, parentKey, paging) })
392
+ ;(payload.expandedIds || []).forEach(id => { this.$set(this.expanded, id, true) })
393
+ },
394
+ unique(values) { return [...new Set(values)] },
395
+ getJson(url, params = {}) {
396
+ const requestUrl = new URL(url, window.location.origin)
397
+ Object.entries(params).forEach(([key, value]) => {
398
+ if (value !== null && value !== undefined && value !== '') requestUrl.searchParams.set(key, value)
399
+ })
400
+ return fetch(requestUrl.toString(), { headers: this.requestHeaders(), credentials: 'same-origin' }).then(this.parseResponse)
401
+ },
402
+ requestHeaders(extra = {}) {
403
+ const kompoInfo = this.kompoInfoHeader()
404
+ return {
405
+ 'X-Requested-With': 'XMLHttpRequest',
406
+ ...(kompoInfo ? { 'X-Kompo-Info': kompoInfo } : {}),
407
+ ...extra,
408
+ }
409
+ },
410
+ kompoInfoHeader() {
411
+ if (this.kompoinfo) return this.kompoinfo
412
+ if (this.$_kompoInfo) return this.$_kompoInfo
413
+ if (this.kompoInfo) return this.kompoInfo
414
+ return ''
415
+ },
416
+ parseResponse(response) {
417
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`)
418
+ return response.json()
419
+ },
420
+ handleError(error, seq = this.requestSeq) {
421
+ if (seq !== this.requestSeq) return
422
+ this.error = error.message || this.labels.error
423
+ this.$emit('error', this.error)
424
+ },
425
+ kompoElementAttributes(component, item) {
426
+ return {
427
+ key: component.id || item.key,
428
+ is: this.vueTag(component),
429
+ vkompo: component,
430
+ kompoid: this.kompoid || this.$_elKompoId,
431
+ }
432
+ },
433
+ vueTag(component) { return 'Vl' + component.vueComponent },
434
+ },
435
+ }
436
+ </script>
437
+
438
+ <style>
439
+ .lazy-hierarchy {
440
+ --lazy-hierarchy-primary: var(--level1, var(--greenmain, var(--primary, currentColor)));
441
+ --lazy-hierarchy-primary-dark: var(--greendark, var(--primary-dark, var(--lazy-hierarchy-primary)));
442
+ --lazy-hierarchy-primary-soft: color-mix(in srgb, var(--lazy-hierarchy-primary) 9%, transparent);
443
+ --lazy-hierarchy-primary-soft-hover: color-mix(in srgb, var(--lazy-hierarchy-primary) 13%, transparent);
444
+ position: relative;
445
+ display: block;
446
+ color: var(--lazy-hierarchy-primary);
447
+ }
448
+
449
+ .lazy-hierarchy--dropdown { display: inline-flex; }
450
+
451
+ .lazy-hierarchy__trigger {
452
+ display: inline-flex;
453
+ align-items: center;
454
+ gap: 0.35rem;
455
+ border: 0;
456
+ background: transparent;
457
+ color: #1f2933;
458
+ font-size: 0.875rem;
459
+ font-weight: 600;
460
+ line-height: 1.2;
461
+ cursor: pointer;
462
+ padding: 0.25rem 0.35rem;
463
+ }
464
+
465
+ .lazy-hierarchy__trigger-label {
466
+ max-width: 12rem;
467
+ overflow: hidden;
468
+ text-overflow: ellipsis;
469
+ white-space: nowrap;
470
+ }
471
+
472
+ .lazy-hierarchy__trigger-icon {
473
+ width: 1rem;
474
+ height: 1rem;
475
+ flex: 0 0 1rem;
476
+ display: block;
477
+ color: var(--lazy-hierarchy-primary);
478
+ transform: translateY(0.05rem);
479
+ transform-origin: center;
480
+ transition: transform 0.2s ease;
481
+ }
482
+ .lazy-hierarchy__trigger-icon--open { transform: translateY(0.05rem) rotate(180deg); }
483
+
484
+ .lazy-hierarchy__panel {
485
+ position: absolute;
486
+ top: calc(100% + 0.45rem);
487
+ right: 0;
488
+ width: 33rem;
489
+ max-width: calc(100vw - 1rem);
490
+ max-height: min(28rem, calc(100vh - 4rem));
491
+ display: flex;
492
+ flex-direction: column;
493
+ background: #fff;
494
+ border: 1px solid #e7ece9;
495
+ border-radius: 8px;
496
+ box-shadow: 0 10px 24px rgba(17, 24, 39, 0.11);
497
+ z-index: 940;
498
+ overflow: hidden;
499
+ }
500
+ .lazy-hierarchy--inline .lazy-hierarchy__panel {
501
+ position: static;
502
+ width: 100%;
503
+ max-width: none;
504
+ max-height: none;
505
+ border: 0;
506
+ box-shadow: none;
507
+ }
508
+
509
+ .lazy-hierarchy__top {
510
+ display: flex;
511
+ align-items: flex-start;
512
+ gap: 1.25rem;
513
+ flex: 0 0 auto;
514
+ padding: 1.5rem;
515
+ border-bottom: 1px solid #edf1ef;
516
+ }
517
+
518
+ .lazy-hierarchy__search-wrap { min-width: 0; flex: 1 1 auto; margin: 0; }
519
+ .lazy-hierarchy__search { width: 100%; min-height: 2.35rem; font-size: 0.8125rem; }
520
+
521
+ .lazy-hierarchy__modes { display: flex; flex-direction: column; gap: 0.5rem; flex: 0 0 auto; }
522
+ .lazy-hierarchy__mode {
523
+ display: flex;
524
+ align-items: center;
525
+ color: #334155;
526
+ font-size: 0.75rem;
527
+ line-height: 1;
528
+ cursor: pointer;
529
+ white-space: nowrap;
530
+ min-height: 1.35rem;
531
+ }
532
+ .lazy-hierarchy__radio-control {
533
+ width: 1.15rem !important;
534
+ min-width: 1.15rem !important;
535
+ max-width: 1.15rem !important;
536
+ height: 1.15rem !important;
537
+ min-height: 1.15rem !important;
538
+ max-height: 1.15rem !important;
539
+ flex: 0 0 1.15rem;
540
+ display: inline-block;
541
+ position: relative;
542
+ margin-right: 0.5rem;
543
+ padding: 0;
544
+ background: #fff;
545
+ border: 0;
546
+ border-radius: 9999px;
547
+ box-shadow: none;
548
+ overflow: hidden;
549
+ }
550
+ .lazy-hierarchy__radio-control::after { display: none; }
551
+ .lazy-hierarchy__mode input {
552
+ position: absolute;
553
+ inset: 0;
554
+ z-index: 2;
555
+ width: 100%;
556
+ height: 100%;
557
+ margin: 0;
558
+ padding: 0;
559
+ appearance: none;
560
+ border: 0;
561
+ border-radius: 9999px;
562
+ background: transparent;
563
+ color: var(--lazy-hierarchy-primary);
564
+ cursor: pointer;
565
+ opacity: 0;
566
+ }
567
+ .lazy-hierarchy__radio-dot {
568
+ position: absolute;
569
+ inset: 0;
570
+ border: 1px solid #d9e3df;
571
+ border-radius: 9999px;
572
+ background: #fff;
573
+ pointer-events: none;
574
+ }
575
+ .lazy-hierarchy__radio-dot::after {
576
+ content: "";
577
+ position: absolute;
578
+ inset: 0.32rem;
579
+ border-radius: 9999px;
580
+ background: transparent;
581
+ }
582
+ .lazy-hierarchy__mode input:checked + .lazy-hierarchy__radio-dot {
583
+ border-color: var(--lazy-hierarchy-primary);
584
+ background: var(--lazy-hierarchy-primary);
585
+ }
586
+ .lazy-hierarchy__mode input:checked + .lazy-hierarchy__radio-dot::after { background: #fff; }
587
+ .lazy-hierarchy__radio-control:focus-within { box-shadow: 0 0 0 3px var(--lazy-hierarchy-primary-soft-hover); }
588
+ .lazy-hierarchy__mode-label { margin: 0; color: inherit; font-size: 0.75rem; line-height: 1; }
589
+ .lazy-hierarchy__mode--active { color: var(--lazy-hierarchy-primary); font-weight: 700; }
590
+
591
+ .lazy-hierarchy__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0.45rem 0 0.75rem; }
592
+
593
+ .lazy-hierarchy__status {
594
+ display: flex;
595
+ flex-direction: column;
596
+ align-items: center;
597
+ justify-content: center;
598
+ gap: 0.65rem;
599
+ padding: 1.1rem 0.65rem;
600
+ color: #64748b;
601
+ font-size: 0.8125rem;
602
+ text-align: center;
603
+ }
604
+ .lazy-hierarchy__status--error { color: #b42318; }
605
+ .lazy-hierarchy__status-label { display: inline-flex; align-items: center; gap: 0.5rem; }
606
+
607
+ .lazy-hierarchy__spinner {
608
+ width: 0.9rem;
609
+ height: 0.9rem;
610
+ border: 2px solid currentColor;
611
+ border-right-color: transparent;
612
+ border-radius: 9999px;
613
+ animation: lazy-hierarchy-spin 0.65s linear infinite;
614
+ }
615
+
616
+ .lazy-hierarchy__sr-only {
617
+ position: absolute;
618
+ width: 1px;
619
+ height: 1px;
620
+ padding: 0;
621
+ overflow: hidden;
622
+ clip: rect(0, 0, 0, 0);
623
+ white-space: nowrap;
624
+ border: 0;
625
+ }
626
+
627
+ @keyframes lazy-hierarchy-spin { to { transform: rotate(360deg); } }
628
+
629
+ .lazy-hierarchy__skeleton-row {
630
+ width: 100%;
631
+ min-height: 2.35rem;
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 0.45rem;
635
+ }
636
+ .lazy-hierarchy__skeleton-name,
637
+ .lazy-hierarchy__skeleton-pill {
638
+ display: inline-block;
639
+ height: 0.8rem;
640
+ border-radius: 9999px;
641
+ background: linear-gradient(90deg, #eef4f1 0%, #f8faf9 50%, #eef4f1 100%);
642
+ background-size: 180% 100%;
643
+ animation: lazy-hierarchy-skeleton 1.1s ease-in-out infinite;
644
+ }
645
+ .lazy-hierarchy__skeleton-name { flex: 1 1 auto; }
646
+ .lazy-hierarchy__skeleton-pill { width: 4.2rem; }
647
+ @keyframes lazy-hierarchy-skeleton {
648
+ 0% { background-position: 100% 0; }
649
+ 100% { background-position: -100% 0; }
650
+ }
651
+
652
+ .lazy-hierarchy-node__row {
653
+ min-height: 2.55rem;
654
+ display: flex;
655
+ align-items: center;
656
+ gap: 0.45rem;
657
+ padding-top: 0.2rem;
658
+ padding-right: 1rem;
659
+ padding-bottom: 0.2rem;
660
+ color: var(--lazy-hierarchy-primary);
661
+ }
662
+ .lazy-hierarchy-node--current > .lazy-hierarchy-node__row { background: var(--lazy-hierarchy-primary-soft); }
663
+ .lazy-hierarchy-node--current > .lazy-hierarchy-node__row:hover { background: var(--lazy-hierarchy-primary-soft-hover); }
664
+ .lazy-hierarchy-node__row:hover { background: #f8faf9; }
665
+
666
+ .lazy-hierarchy-node__content { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.45rem; }
667
+
668
+ .lazy-hierarchy-node__toggle,
669
+ .lazy-hierarchy-node__toggle-spacer {
670
+ width: 1rem;
671
+ height: 1.35rem;
672
+ flex: 0 0 1rem;
673
+ }
674
+ .lazy-hierarchy-node__toggle {
675
+ display: inline-flex;
676
+ align-items: center;
677
+ justify-content: center;
678
+ border: 0;
679
+ background: transparent;
680
+ color: var(--lazy-hierarchy-primary-dark);
681
+ line-height: 1;
682
+ cursor: pointer;
683
+ }
684
+ .lazy-hierarchy-node__chevron {
685
+ width: 0.9rem;
686
+ height: 0.9rem;
687
+ flex: 0 0 0.9rem;
688
+ transition: transform 0.2s ease;
689
+ }
690
+ .lazy-hierarchy-node__chevron--open { transform: rotate(90deg); }
691
+
692
+ /* Generic visual primitives for node-render composition.
693
+ Factories render their rows as trees containing these classes;
694
+ specific colors are applied inline via kompo's ->style() on the element. */
695
+
696
+ .lazy-hierarchy__badge {
697
+ display: inline-block;
698
+ min-width: 3.8rem;
699
+ padding: 0.25rem 0.75rem;
700
+ text-align: center;
701
+ border-radius: 9999px;
702
+ font-size: 0.75rem;
703
+ font-weight: 500;
704
+ line-height: 1;
705
+ white-space: nowrap;
706
+ color: #fff;
707
+ }
708
+
709
+ .lazy-hierarchy__chip-button {
710
+ display: inline-block;
711
+ max-width: 6rem;
712
+ overflow: hidden;
713
+ text-overflow: ellipsis;
714
+ padding: 0.25rem 0.75rem;
715
+ border: 0;
716
+ border-radius: 9999px;
717
+ background: transparent;
718
+ color: var(--lazy-hierarchy-primary);
719
+ font-size: 0.75rem;
720
+ font-weight: 500;
721
+ line-height: 1;
722
+ white-space: nowrap;
723
+ text-decoration: none;
724
+ cursor: pointer;
725
+ transition: background 0.15s ease;
726
+ }
727
+ .lazy-hierarchy__chip-button:hover {
728
+ background: color-mix(in srgb, var(--lazy-hierarchy-primary) 8%, white);
729
+ text-decoration: none;
730
+ }
731
+ .lazy-hierarchy__chip-button--current {
732
+ background: var(--lazy-hierarchy-primary);
733
+ color: #fff;
734
+ }
735
+ .lazy-hierarchy__chip-button--current:hover {
736
+ background: var(--lazy-hierarchy-primary);
737
+ color: #fff;
738
+ }
739
+
740
+ .lazy-hierarchy__icon-button {
741
+ display: inline-flex;
742
+ align-items: center;
743
+ justify-content: center;
744
+ width: 1.65rem;
745
+ height: 1.65rem;
746
+ padding: 0;
747
+ border: 0;
748
+ border-radius: 6px;
749
+ background: transparent;
750
+ color: var(--lazy-hierarchy-primary);
751
+ text-decoration: none;
752
+ cursor: pointer;
753
+ transition: background 0.15s ease;
754
+ }
755
+ .lazy-hierarchy__icon-button:hover {
756
+ background: color-mix(in srgb, var(--lazy-hierarchy-primary) 8%, white);
757
+ text-decoration: none;
758
+ }
759
+ .lazy-hierarchy__icon-button svg {
760
+ display: block;
761
+ width: 1.25rem;
762
+ height: 1.25rem;
763
+ }
764
+
765
+ .lazy-hierarchy-node__loading {
766
+ display: flex;
767
+ align-items: center;
768
+ padding: 0.25rem 0.55rem;
769
+ color: #64748b;
770
+ font-size: 0.75rem;
771
+ }
772
+
773
+ .lazy-hierarchy__show-more {
774
+ display: flex;
775
+ width: calc(100% - 1rem);
776
+ margin: 0.55rem 0.5rem 0.35rem;
777
+ border: 1px solid #dbe4df;
778
+ border-radius: 6px;
779
+ background: #fff;
780
+ color: var(--lazy-hierarchy-primary);
781
+ cursor: pointer;
782
+ font-size: 0.75rem;
783
+ font-weight: 700;
784
+ padding: 0.5rem;
785
+ }
786
+ .lazy-hierarchy__show-more:hover { background: #f5fbf7; }
787
+
788
+ .lazy-hierarchy button:disabled { cursor: wait; opacity: 0.65; }
789
+
790
+ @media (max-width: 640px) {
791
+ .lazy-hierarchy__panel {
792
+ top: calc(100% + 0.35rem);
793
+ right: -0.25rem;
794
+ width: calc(100vw - 1rem);
795
+ }
796
+ .lazy-hierarchy__top { flex-direction: column; }
797
+ .lazy-hierarchy__modes { flex-direction: row; }
798
+ }
799
+ </style>