anentrypoint-design 0.0.384 → 0.0.385

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,179 @@
1
+ // Pure-DOM column state for WorkspaceShell: desktop collapse toggles, the
2
+ // drag/keyboard resize handle and its clamps, the persisted-width seeder, and
3
+ // the mobile drawer open/close (with Esc + focus-trap + viewport auto-close).
4
+ // WorkspaceShell itself is stateless chrome, so all of this lives as classes
5
+ // and inline --ws-*-w vars on .ws-shell rather than in a host store.
6
+
7
+ import * as webjsx from '../../../vendor/webjsx/index.js';
8
+ import { trapTab } from '../overlay-primitives.js';
9
+ const h = webjsx.createElement;
10
+
11
+ // Toggle a named WorkspaceShell column (left rail or right pane). Pure-DOM like
12
+ // toggleSide: WorkspaceShell is stateless chrome, the collapsed class lives on
13
+ // .ws-shell and is read by both CSS and the toggle buttons' aria-expanded.
14
+ export function toggleWs(which, fromEl) {
15
+ // Scope to the shell owning the clicked control, like toggleSide — the
16
+ // first-on-page querySelector toggles the WRONG shell with two instances.
17
+ const shell = (fromEl && fromEl.closest && fromEl.closest('.ws-shell')) || document.querySelector('.ws-shell');
18
+ if (!shell) return;
19
+ const cls = which === 'pane' ? 'ws-pane-collapsed'
20
+ : which === 'sessions' ? 'ws-sessions-collapsed'
21
+ : 'ws-rail-collapsed';
22
+ const nowCollapsed = shell.classList.toggle(cls);
23
+ // Inline --ws-*-w beats the collapsed-class rule in the cascade, so a
24
+ // persisted width would render a "collapsed" column 200-640px wide.
25
+ if (nowCollapsed) shell.style.removeProperty('--ws-' + which + '-w');
26
+ shell.querySelectorAll('.ws-' + which + '-toggle').forEach((btn) => {
27
+ btn.setAttribute('aria-expanded', nowCollapsed ? 'false' : 'true');
28
+ const nextLabel = nowCollapsed ? 'expand ' + which : 'collapse ' + which;
29
+ btn.setAttribute('aria-label', nextLabel);
30
+ btn.setAttribute('title', nextLabel);
31
+ });
32
+ try {
33
+ localStorage.setItem('ds.ws.' + which, nowCollapsed ? 'collapsed' : 'open');
34
+ } catch (_) { /* swallow: persistence is best-effort, collapse state still applies in-memory */ }
35
+ // Expanding restores the persisted width (seed skips collapsed columns, so
36
+ // it must run after the open flag is written).
37
+ if (!nowCollapsed) seedWsWidths(shell);
38
+ }
39
+
40
+ // Column resize: read the current rendered track width and write a clamped inline
41
+ // --ws-<col>-w on .ws-shell (inline overrides the fluid clamp base), persisted.
42
+ // Floors match the CSS fluid clamp() floors in app-shell.css (--ws-rail-w
43
+ // clamp(200,16vw,260); sessions clamp(248,22vw,360); pane clamp(288,24vw,420))
44
+ // so a drag/arrow can never shrink a column below its designed minimum (the
45
+ // collapsed rail is a SEPARATE class, not a resize target). The ceilings are
46
+ // INTENTIONALLY raised above the fluid clamp() mid-term ceilings: on wide
47
+ // viewports the 16/22/24vw mid term already pins each column to its clamp
48
+ // ceiling, so a ceiling-equals-clamp bound made the outward drag inert there.
49
+ // The higher resize ceilings let a deliberate drag/arrow grow a column past its
50
+ // auto-fluid width (the inline --ws-<col>-w override pins the chosen width past
51
+ // the clamp base).
52
+ const WS_RESIZE_CLAMP = { rail: [200, 320], sessions: [248, 520], pane: [288, 640] };
53
+ function wsResize(col, dx, persist = true, fromEl) {
54
+ const shell = (fromEl && fromEl.closest && fromEl.closest('.ws-shell')) || document.querySelector('.ws-shell');
55
+ if (!shell) return;
56
+ const track = shell.querySelector('.ws-' + col);
57
+ const cur = track ? track.getBoundingClientRect().width : 0;
58
+ const [lo, hi] = WS_RESIZE_CLAMP[col] || [120, 600];
59
+ const next = Math.max(lo, Math.min(hi, Math.round(cur + dx)));
60
+ shell.style.setProperty('--ws-' + col + '-w', next + 'px');
61
+ const handle = shell.querySelector('.ws-resizer-' + col);
62
+ if (handle) { handle.setAttribute('aria-valuenow', String(next)); handle.setAttribute('aria-valuetext', next + ' pixels'); }
63
+ // Commit to storage only on a settled move (pointerup / keyboard), not on
64
+ // every pointermove frame (that fired dozens of synchronous writes per drag).
65
+ if (persist) { try { localStorage.setItem('ds.ws.w.' + col, String(next)); } catch (_) { /* swallow: persistence is best-effort, resize still applies in-memory */ } }
66
+ }
67
+ // Per-column viewport caps for persisted widths: a width dragged on a wide
68
+ // monitor must not crush the content column when the page reloads on a
69
+ // narrower screen (rail 320 + sessions 520 would leave ~180px of content).
70
+ const WS_VW_CAP = { rail: '20vw', sessions: '30vw', pane: '32vw' };
71
+ export function seedWsWidths(el) {
72
+ if (!el) return;
73
+ ['rail', 'sessions', 'pane'].forEach((col) => {
74
+ try {
75
+ // A persisted-collapsed column must stay collapsed: the inline var
76
+ // would beat the .ws-*-collapsed class rule in the cascade.
77
+ if (wsCollapsed(col, false)) return;
78
+ const v = localStorage.getItem('ds.ws.w.' + col);
79
+ if (v && /^\d+$/.test(v)) el.style.setProperty('--ws-' + col + '-w', `min(${v}px, ${WS_VW_CAP[col]})`);
80
+ } catch (_) { /* swallow: localStorage unavailable, seeding is best-effort */ }
81
+ });
82
+ }
83
+ export function WsResizer(col) {
84
+ const onKey = (e) => {
85
+ if (e.key === 'ArrowLeft') { e.preventDefault(); wsResize(col, -16, true, e.currentTarget); }
86
+ else if (e.key === 'ArrowRight') { e.preventDefault(); wsResize(col, 16, true, e.currentTarget); }
87
+ };
88
+ const onDown = (e) => {
89
+ e.preventDefault();
90
+ const handleEl = e.currentTarget;
91
+ let lastX = e.clientX;
92
+ const move = (ev) => { const dx = ev.clientX - lastX; lastX = ev.clientX; wsResize(col, dx, false, handleEl); };
93
+ const up = () => {
94
+ document.removeEventListener('pointermove', move);
95
+ document.removeEventListener('pointerup', up);
96
+ document.body.style.cursor = '';
97
+ wsResize(col, 0, true, handleEl); // commit the settled width once
98
+ };
99
+ document.addEventListener('pointermove', move);
100
+ document.addEventListener('pointerup', up);
101
+ document.body.style.cursor = 'col-resize';
102
+ };
103
+ const [lo, hi] = WS_RESIZE_CLAMP[col] || [120, 600];
104
+ // Seed aria-valuenow from the rendered track width so AT announces real widths.
105
+ const seedNow = (el) => {
106
+ if (!el) return;
107
+ const track = el.closest('.ws-shell') && el.closest('.ws-shell').querySelector('.ws-' + col);
108
+ if (track) { const w = Math.round(track.getBoundingClientRect().width); el.setAttribute('aria-valuenow', String(w)); el.setAttribute('aria-valuetext', w + ' pixels'); }
109
+ };
110
+ return h('div', {
111
+ class: 'ws-resizer ws-resizer-' + col, role: 'separator', tabindex: '0',
112
+ 'aria-orientation': 'vertical', 'aria-label': 'resize ' + col + ' column (arrow keys)',
113
+ 'aria-valuemin': String(lo), 'aria-valuemax': String(hi), 'aria-valuetext': String(hi) + ' pixels',
114
+ onpointerdown: onDown, onkeydown: onKey, ref: seedNow,
115
+ });
116
+ }
117
+
118
+ // Toggle a mobile WorkspaceShell DRAWER (sessions or pane). Distinct from the
119
+ // desktop width-collapse (toggleWs): on mobile the columns are fixed overlays
120
+ // revealed by .ws-sessions-open / .ws-pane-open. Opening one closes the other
121
+ // (only one drawer at a time over the content). Esc + scrim dismiss call this
122
+ // with open=false. Pure-DOM, matching the AppShell toggleSide pattern.
123
+ export function toggleWsDrawer(which, open, fromEl) {
124
+ const shell = (fromEl && fromEl.closest && fromEl.closest('.ws-shell')) || document.querySelector('.ws-shell');
125
+ if (!shell) return;
126
+ const cls = which === 'pane' ? 'ws-pane-open' : 'ws-sessions-open';
127
+ const other = which === 'pane' ? 'ws-sessions-open' : 'ws-pane-open';
128
+ const next = open != null ? open : !shell.classList.contains(cls);
129
+ shell.classList.toggle(cls, next);
130
+ if (next) shell.classList.remove(other);
131
+ const btn = shell.querySelector('.ws-' + which + '-drawer-toggle');
132
+ if (btn) btn.setAttribute('aria-expanded', next ? 'true' : 'false');
133
+ if (!next) { removeWsDrawerHandlers(shell); return; }
134
+ // When opening, move focus into the drawer, arm an Esc-to-close, and trap
135
+ // Tab/Shift+Tab inside the drawer (a real focus trap, matching the kit's
136
+ // own dialogs - Tab from inside an open drawer previously walked focus out
137
+ // into the scrim/background content behind it).
138
+ const drawer = shell.querySelector(which === 'pane' ? '.ws-pane' : '.ws-sessions');
139
+ const focusable = drawer && drawer.querySelector('button, a, input, [tabindex]');
140
+ if (focusable) try { focusable.focus(); } catch (_) { /* swallow: focus() can throw on a detached/hidden element, drawer still opens */ }
141
+ removeWsDrawerHandlers(shell); // replace, never stack (opening one drawer over the other)
142
+ const onKey = (e) => {
143
+ if (e.key === 'Escape') { toggleWsDrawer(which, false, shell); if (btn) try { btn.focus(); } catch (_) { /* swallow: focus() can throw on a detached/hidden element */ } return; }
144
+ if (drawer) trapTab(drawer, e);
145
+ };
146
+ shell._wsEscHandler = onKey;
147
+ document.addEventListener('keydown', onKey);
148
+ // The drawer CSS stops applying above its breakpoint; auto-close when the
149
+ // viewport grows past it so the open class and armed Esc/focus-trap
150
+ // handlers do not linger invisibly in desktop layout.
151
+ const mq = window.matchMedia('(max-width: 1480px)');
152
+ const onMq = () => { if (!mq.matches) closeWsDrawers(shell); };
153
+ shell._wsDrawerMq = { mq, onMq };
154
+ mq.addEventListener('change', onMq);
155
+ }
156
+ function removeWsDrawerHandlers(shell) {
157
+ // Remove Esc/focus-trap handler armed by toggleWsDrawer (prevents ghost
158
+ // close on next Esc) and the viewport-growth auto-close listener.
159
+ if (shell._wsEscHandler) { document.removeEventListener('keydown', shell._wsEscHandler); shell._wsEscHandler = null; }
160
+ if (shell._wsDrawerMq) { shell._wsDrawerMq.mq.removeEventListener('change', shell._wsDrawerMq.onMq); shell._wsDrawerMq = null; }
161
+ }
162
+ export function closeWsDrawers(fromEl) {
163
+ const shell = (fromEl && fromEl.closest && fromEl.closest('.ws-shell')) || document.querySelector('.ws-shell');
164
+ if (!shell) return;
165
+ shell.classList.remove('ws-sessions-open', 'ws-pane-open');
166
+ shell.querySelectorAll('.ws-sessions-drawer-toggle, .ws-pane-drawer-toggle').forEach((b) => b.setAttribute('aria-expanded', 'false'));
167
+ removeWsDrawerHandlers(shell);
168
+ }
169
+
170
+ // Read persisted collapse state for a WorkspaceShell column so the layout is
171
+ // predictable across reloads (Claude-Desktop keeps the rail where you left it).
172
+ export function wsCollapsed(which, fallback) {
173
+ try {
174
+ const v = localStorage.getItem('ds.ws.' + which);
175
+ if (v === 'collapsed') return true;
176
+ if (v === 'open') return false;
177
+ } catch (_) { /* swallow: localStorage unavailable, fall back to caller-supplied default */ }
178
+ return !!fallback;
179
+ }
@@ -0,0 +1,181 @@
1
+ // The Claude-Desktop / cowork multi-column app frame: WorkspaceShell (rail +
2
+ // optional sessions + main + optional context pane) and WorkspaceRail (the
3
+ // rail's own brand/action/nav contents). Both are pure stateless chrome —
4
+ // every collapse/resize/drawer behaviour lives in ./workspace-columns.js.
5
+
6
+ import * as webjsx from '../../../vendor/webjsx/index.js';
7
+ import { Icon } from './icons.js';
8
+ import { toggleWs, toggleWsDrawer, closeWsDrawers, wsCollapsed, seedWsWidths, WsResizer } from './workspace-columns.js';
9
+ const h = webjsx.createElement;
10
+
11
+ /**
12
+ * A Claude-Desktop / cowork three-(or four-)column app shell.
13
+ *
14
+ * Pure stateless chrome (props in, vnode out). Collapse is DOM-class + a
15
+ * persisted flag, so the host does not have to thread collapse state through
16
+ * its own store. Visual styling lives in app-shell.css (.ws-*).
17
+ *
18
+ * @param {Object} props
19
+ * @param {*} props.rail - the persistent left workspace nav (icon+label items, collapsible to icon-only). Pass the result of WorkspaceRail() or any vnode.
20
+ * @param {*} props.sessions - an OPTIONAL second column (a conversation/session list) shown between the rail and the main content. Null hides it.
21
+ * @param {*} props.main - the primary content column (chat thread, files view, dashboard...).
22
+ * @param {*} props.pane - an OPTIONAL right context pane (per-conversation context, file preview...). Null hides it; collapsible when present.
23
+ * @param {*} props.crumb - an optional thin top chrome bar (breadcrumb + status), spanning the content area only (the rail has its own header).
24
+ * @param {*} props.status - an optional footer.
25
+ * @param {boolean} props.narrow - caller's isNarrow() — drives the mobile single-column collapse.
26
+ * @param {boolean} props.railCollapsed - initial rail collapse (persisted state wins).
27
+ * @param {boolean} props.paneCollapsed - initial pane collapse (persisted state wins).
28
+ * @returns {*} webjsx vnode
29
+ */
30
+ export function WorkspaceShell({ rail, sessions, main, pane, crumb, status, narrow,
31
+ railCollapsed = false, paneCollapsed = false,
32
+ railLabel = 'workspace navigation',
33
+ paneLabel = 'context', stableFrame = false, mainFlush = false } = {}) {
34
+ const hasSessions = Boolean(sessions);
35
+ const hasPane = Boolean(pane);
36
+ // Stable frame: keep the pane grid TRACK present even when this tab has no
37
+ // pane, so the shell does not re-flow its column count (4/3/2) on every tab
38
+ // switch - the loudest "separate pages" tell. The track collapses to width 0
39
+ // (ws-pane-collapsed) instead of being removed (ws-no-pane), so chat/history/
40
+ // files/live/settings all keep the same column geometry. The sessions column
41
+ // gets the identical treatment (ws-sessions-collapsed instead of ws-no-sessions)
42
+ // so files/live/settings do not shift the main column when sessions is null.
43
+ const keepPaneTrack = stableFrame && !hasPane;
44
+ const keepSessionsTrack = stableFrame && !hasSessions;
45
+ const railIsCollapsed = wsCollapsed('rail', railCollapsed);
46
+ const paneIsCollapsed = hasPane ? wsCollapsed('pane', paneCollapsed) : true;
47
+ const sessionsIsCollapsed = hasSessions ? wsCollapsed('sessions', false) : true;
48
+ const shellCls = 'ws-shell'
49
+ + (railIsCollapsed ? ' ws-rail-collapsed' : '')
50
+ + ((hasPane || keepPaneTrack) ? '' : ' ws-no-pane')
51
+ + (((hasPane && paneIsCollapsed) || keepPaneTrack) ? ' ws-pane-collapsed' : '')
52
+ + ((hasSessions || keepSessionsTrack) ? '' : ' ws-no-sessions')
53
+ + (((hasSessions && sessionsIsCollapsed) || keepSessionsTrack) ? ' ws-sessions-collapsed' : '')
54
+ + (narrow ? ' narrow' : '');
55
+ return h('div', { class: shellCls, ref: seedWsWidths },
56
+ h('a', { href: '#ws-main', class: 'skip-link' }, 'skip to main content'),
57
+ // Left rail column. Its own toggle collapses it to icon-only.
58
+ h('nav', { class: 'ws-rail', role: 'navigation', 'aria-label': railLabel },
59
+ h('button', {
60
+ class: 'ws-rail-toggle', type: 'button',
61
+ // Label reflects the ACTION the click performs (expand when
62
+ // collapsed, collapse when expanded), not a static word - a
63
+ // stale "collapse navigation" on an already-collapsed rail
64
+ // mis-announces the control to AT.
65
+ 'aria-label': railIsCollapsed ? 'expand navigation' : 'collapse navigation',
66
+ title: railIsCollapsed ? 'expand navigation' : 'collapse navigation',
67
+ 'aria-expanded': railIsCollapsed ? 'false' : 'true',
68
+ onclick: (e) => toggleWs('rail', e.currentTarget),
69
+ }, Icon('menu')),
70
+ rail || null),
71
+ // Tap-scrim behind an open mobile drawer; click anywhere dismisses.
72
+ h('div', { class: 'ws-scrim', 'aria-hidden': 'true', onclick: (e) => closeWsDrawers(e.currentTarget) }),
73
+ // Optional sessions column. On mobile it is a drawer; selecting a row
74
+ // (any button click inside) auto-closes it, mirroring AppShell.
75
+ hasSessions
76
+ ? h('div', { id: 'ws-sessions-col', class: 'ws-sessions', role: 'complementary', 'aria-label': 'conversations',
77
+ // Drawer mode is detected by geometry (position:fixed only holds
78
+ // in drawer mode), not window.innerWidth - the shell may live in
79
+ // an embedded window narrower than the viewport.
80
+ onclick: (e) => {
81
+ const col = e.currentTarget;
82
+ if (getComputedStyle(col).position === 'fixed' && e.target.closest('button, a, [role="button"]')) closeWsDrawers(col);
83
+ } }, sessions)
84
+ : null,
85
+ // Primary content column, with an optional thin crumb bar on top. On
86
+ // mobile the crumb hosts the drawer toggles (sessions on the left, pane
87
+ // on the right) so both overlay columns are reachable - without them the
88
+ // conversation list and context pane are dead on <=900px.
89
+ h('div', { class: 'ws-content' },
90
+ crumb
91
+ ? h('div', { class: 'ws-crumb' },
92
+ hasSessions ? h('button', {
93
+ class: 'ws-drawer-toggle ws-sessions-drawer-toggle', type: 'button',
94
+ 'aria-label': 'toggle conversations', 'aria-expanded': 'false',
95
+ 'aria-controls': 'ws-sessions-col',
96
+ onclick: (e) => toggleWsDrawer('sessions', null, e.currentTarget),
97
+ }, Icon('thread')) : null,
98
+ // Desktop-only sessions collapse (reclaims its width for a
99
+ // full-width thread/grid). Hidden on mobile via CSS.
100
+ hasSessions ? h('button', {
101
+ class: 'ws-desktop-toggle ws-sessions-toggle', type: 'button',
102
+ 'aria-label': sessionsIsCollapsed ? 'expand conversations' : 'collapse conversations',
103
+ title: sessionsIsCollapsed ? 'expand conversations' : 'collapse conversations',
104
+ 'aria-expanded': sessionsIsCollapsed ? 'false' : 'true', onclick: (e) => toggleWs('sessions', e.currentTarget),
105
+ }, Icon(sessionsIsCollapsed ? 'chevron-right' : 'chevron-left')) : null,
106
+ h('div', { class: 'ws-crumb-main' }, crumb),
107
+ // Desktop-only context-pane collapse, on the same crumb-level
108
+ // chrome idiom as the sessions toggle. Hidden on mobile via CSS.
109
+ hasPane ? h('button', {
110
+ class: 'ws-desktop-toggle ws-pane-toggle', type: 'button',
111
+ 'aria-label': paneIsCollapsed ? 'show context pane' : 'hide context pane',
112
+ title: paneIsCollapsed ? 'show context pane' : 'hide context pane',
113
+ 'aria-expanded': paneIsCollapsed ? 'false' : 'true',
114
+ onclick: (e) => toggleWs('pane', e.currentTarget),
115
+ }, Icon(paneIsCollapsed ? 'chevron-left' : 'chevron-right')) : null,
116
+ hasPane ? h('button', {
117
+ class: 'ws-drawer-toggle ws-pane-drawer-toggle', type: 'button',
118
+ 'aria-label': 'toggle context pane', 'aria-expanded': 'false',
119
+ 'aria-controls': 'ws-pane-col',
120
+ onclick: (e) => toggleWsDrawer('pane', null, e.currentTarget),
121
+ }, Icon('page')) : null)
122
+ : null,
123
+ h('main', { class: 'ws-main' + (narrow ? ' narrow' : '') + (mainFlush ? ' ws-main--flush' : ''), id: 'ws-main', tabindex: '-1' },
124
+ ...(Array.isArray(main) ? main : [main])),
125
+ status || null),
126
+ // Optional right context pane. Its desktop collapse toggle now lives in
127
+ // the crumb cluster, alongside the sessions toggle.
128
+ hasPane
129
+ ? h('aside', { id: 'ws-pane-col', class: 'ws-pane', role: 'complementary', 'aria-label': paneLabel },
130
+ pane)
131
+ : null,
132
+ // Keyboard/pointer column resize handles (desktop only).
133
+ (!narrow && !railIsCollapsed) ? WsResizer('rail') : null,
134
+ (!narrow && (hasSessions || keepSessionsTrack) && !sessionsIsCollapsed) ? WsResizer('sessions') : null,
135
+ (!narrow && (hasPane || keepPaneTrack) && !paneIsCollapsed) ? WsResizer('pane') : null,
136
+ );
137
+ }
138
+
139
+ // WorkspaceRail — the contents of the WorkspaceShell left rail: a brand/header,
140
+ // a primary action (New chat), and a list of nav items. Each item collapses to
141
+ // an icon when the rail is collapsed (the label is kept in the DOM for AT and
142
+ // shown via CSS when expanded).
143
+ //
144
+ // brand : short product name shown in the rail header.
145
+ // action : { label, icon, onClick } a prominent primary button (New chat).
146
+ // items : [{ key, label, icon, active, count, rail, onClick }] nav entries.
147
+ // `rail` (optional tone e.g. 'flame') paints an attention dot on the
148
+ // item — used when something in that surface needs the user's eyes
149
+ // even though they are looking at a different tab (e.g. a live
150
+ // session in error while the user is in Chat).
151
+ // footer : optional vnode pinned to the rail bottom (e.g. settings/theme).
152
+ export function WorkspaceRail({ brand = '247420', action, items = [], footer } = {}) {
153
+ return h('div', { class: 'ws-rail-inner' },
154
+ h('div', { class: 'ws-rail-head' },
155
+ h('span', { class: 'ws-rail-brand' }, brand)),
156
+ action
157
+ ? h('button', {
158
+ class: 'ws-rail-action', type: 'button',
159
+ 'aria-label': action.label,
160
+ onclick: action.onClick || null,
161
+ }, action.icon ? Icon(action.icon) : null, h('span', { class: 'ws-rail-action-label' }, action.label))
162
+ : null,
163
+ h('ul', { class: 'ws-rail-nav', role: 'list' },
164
+ ...items.map((it) => h('li', { key: it.key || it.label, role: 'listitem' },
165
+ h('button', {
166
+ type: 'button',
167
+ class: 'ws-rail-item' + (it.active ? ' active' : '') + (it.rail ? ' has-rail-flag' : ''),
168
+ 'aria-current': it.active ? 'page' : null,
169
+ 'aria-label': it.label + (it.count ? ' (' + it.count + ')' : '') + (it.rail === 'flame' ? ', needs attention' : ''),
170
+ title: it.label,
171
+ onclick: it.onClick || null,
172
+ },
173
+ it.icon ? Icon(it.icon) : h('span', { class: 'ws-rail-item-glyph', 'aria-hidden': 'true' }),
174
+ h('span', { class: 'ws-rail-item-label' }, it.label),
175
+ (it.count != null && it.count !== 0 && it.count !== '0')
176
+ ? h('span', { class: 'ws-rail-item-count', 'aria-hidden': 'true' }, String(it.count))
177
+ : null,
178
+ it.rail ? h('span', { class: 'ws-rail-item-flag tone-' + it.rail, 'aria-hidden': 'true' }) : null)))),
179
+ footer ? h('div', { class: 'ws-rail-foot' }, footer) : null,
180
+ );
181
+ }