terrier-engine 4.71.7 → 4.72.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.
@@ -228,7 +228,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
228
228
  }
229
229
  })
230
230
 
231
- parent.div(".tt-panel.table-panel", panel => {
231
+ parent.div(".tt-panel.table-panel.stay-as-panel", panel => {
232
232
  panel.div('.title', title => {
233
233
  const t = this.table as any
234
234
  if ('join_type' in t) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.71.7",
7
+ "version": "4.72.0",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
package/terrier/tabs.ts CHANGED
@@ -181,26 +181,32 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
181
181
  }
182
182
  parent.div('tt-tab-container', this.state.side, container => {
183
183
  container.div('.tt-flex.tt-tab-list', tabList => {
184
- tabList.class(`tablist-${this.id}`)
185
184
  if (this._beforeActions.length) {
186
185
  this.theme.renderActions(tabList, this._beforeActions, { defaultClass: 'action' })
187
186
  }
188
- for (const tabKey of this.tabOrder) {
189
- const tab = this.tabs.get(tabKey)!
190
- if (tab.state == 'hidden') continue
191
-
192
- tabList.a('.tab', a => {
193
- if (tab.tabClasses?.length) a.class(...tab.tabClasses)
194
- a.attrs({ draggable: this.state.reorderable })
195
- a.data({ key: tab.key })
196
- a.class(tab.state || 'enabled')
197
- if (tab.key === currentTabKey) a.class('active')
198
- if (tab.icon) this.theme.renderIcon(a, tab.icon, tab.iconColor ? tab.iconColor : 'secondary')
199
- a.span({ text: tab.title })
200
- a.emitClick(this.changeTabKey, { tabKey: tab.key })
201
- if (tab.click) a.emitClick(tab.click.key, tab.click.data || {})
187
+ // The tabs live inside a scroll viewport/track so that they can be shifted
188
+ // horizontally (for top/bottom sides) without moving the before/after actions.
189
+ tabList.div('.tt-tab-scroll', scroll => {
190
+ scroll.div('.tt-tab-track', track => {
191
+ track.class(`tablist-${this.id}`)
192
+ for (const tabKey of this.tabOrder) {
193
+ const tab = this.tabs.get(tabKey)!
194
+ if (tab.state == 'hidden') continue
195
+
196
+ track.a('.tab', a => {
197
+ if (tab.tabClasses?.length) a.class(...tab.tabClasses)
198
+ a.attrs({ draggable: this.state.reorderable })
199
+ a.data({ key: tab.key })
200
+ a.class(tab.state || 'enabled')
201
+ if (tab.key === currentTabKey) a.class('active')
202
+ if (tab.icon) this.theme.renderIcon(a, tab.icon, tab.iconColor ? tab.iconColor : 'secondary')
203
+ a.span({ text: tab.title })
204
+ a.emitClick(this.changeTabKey, { tabKey: tab.key })
205
+ if (tab.click) a.emitClick(tab.click.key, tab.click.data || {})
206
+ })
207
+ }
202
208
  })
203
- }
209
+ })
204
210
  if (this._afterActions.length) {
205
211
  tabList.div('.spacer')
206
212
  this.theme.renderActions(tabList, this._afterActions, { defaultClass: 'action' })
@@ -217,6 +223,92 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
217
223
  })
218
224
  }
219
225
 
226
+ private tabScrollObserver?: ResizeObserver
227
+
228
+ update(elem: HTMLElement) {
229
+ super.update(elem)
230
+ this.layoutHorizontalTabs()
231
+ }
232
+
233
+ /**
234
+ * For top/bottom tab containers, wires up the horizontal scroll behavior: keeps the selected
235
+ * tab in view, translates vertical mouse-wheel gestures into horizontal scrolling, and keeps
236
+ * the selected tab visible as the container is resized.
237
+ */
238
+ private layoutHorizontalTabs() {
239
+ const container = this.element?.querySelector('.tt-tab-container')
240
+ const isHorizontal = !!container && (container.classList.contains('top') || container.classList.contains('bottom'))
241
+ if (!isHorizontal) {
242
+ this.tabScrollObserver?.disconnect()
243
+ this.tabScrollObserver = undefined
244
+ return
245
+ }
246
+
247
+ const viewport = container!.querySelector('.tt-tab-scroll') as HTMLElement | null
248
+ if (!viewport) return
249
+
250
+ // The viewport is recreated on every full render, so the observer has to be re-pointed and
251
+ // the wheel handler re-attached each time update() runs. The dataset flag prevents attaching
252
+ // the listener more than once when the same element survives a stale update.
253
+ if (!this.tabScrollObserver) {
254
+ this.tabScrollObserver = new ResizeObserver(() => this.applyTabScrollOffset())
255
+ }
256
+ this.tabScrollObserver.disconnect()
257
+ this.tabScrollObserver.observe(viewport)
258
+
259
+ if (!viewport.dataset.wheelBound) {
260
+ viewport.dataset.wheelBound = 'true'
261
+ viewport.addEventListener('wheel', this.onTabWheel, { passive: false })
262
+ }
263
+
264
+ this.applyTabScrollOffset()
265
+ }
266
+
267
+ /**
268
+ * Lets a plain (vertical) mouse wheel scroll the tabs horizontally, so users without a
269
+ * trackpad can still scroll left and right. Only intercepts when the tabs actually overflow.
270
+ */
271
+ private onTabWheel = (e: WheelEvent) => {
272
+ if (e.ctrlKey) return // allow browser zoom
273
+ const viewport = e.currentTarget as HTMLElement
274
+ if (viewport.scrollWidth <= viewport.clientWidth) return // nothing to scroll
275
+ const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY
276
+ if (!delta) return
277
+ viewport.scrollLeft += delta
278
+ e.preventDefault()
279
+ }
280
+
281
+ /**
282
+ * Scrolls the viewport so the selected tab sits near the left edge, offset slightly so the
283
+ * preceding tab remains partially visible. Only scrolls when the tabs actually overflow their
284
+ * container; otherwise they're left in place.
285
+ */
286
+ private applyTabScrollOffset() {
287
+ const viewport = this.element?.querySelector('.tt-tab-scroll') as HTMLElement | null
288
+ const track = this.element?.querySelector('.tt-tab-track') as HTMLElement | null
289
+ if (!viewport || !track) return
290
+
291
+ const active = track.querySelector('.tab.active') as HTMLElement | null
292
+ const contentWidth = track.scrollWidth
293
+ const viewportWidth = viewport.clientWidth
294
+
295
+ // If all the tabs fit (or nothing's selected), don't move them.
296
+ if (!active || contentWidth <= viewportWidth) {
297
+ viewport.scrollLeft = 0
298
+ return
299
+ }
300
+
301
+ // Line up the left edge of the viewport just before the active tab, leaving a sliver of the
302
+ // preceding tab visible. Clamp so we never scroll past the last tab (no empty gap at the end).
303
+ const prev = active.previousElementSibling as HTMLElement | null
304
+ let offset = active.offsetLeft
305
+ if (prev) {
306
+ offset = prev.offsetLeft + prev.offsetWidth - Math.min(prev.offsetWidth, 32)
307
+ }
308
+ const maxOffset = contentWidth - viewportWidth
309
+ viewport.scrollLeft = Math.max(0, Math.min(offset, maxOffset))
310
+ }
311
+
220
312
  }
221
313
 
222
314
  const Tabs = {