@rozie-ui/data-table-lit 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.
- package/LICENSE +21 -0
- package/README.md +498 -0
- package/dist/index.cjs +5631 -0
- package/dist/index.d.cts +1120 -0
- package/dist/index.d.mts +1120 -0
- package/dist/index.mjs +5615 -0
- package/package.json +96 -0
- package/src/Column.ts +159 -0
- package/src/DataTable.ts +5473 -0
- package/src/DetailPanel.ts +53 -0
- package/src/EditorCheckbox.ts +67 -0
- package/src/EditorDate.ts +94 -0
- package/src/EditorNumber.ts +95 -0
- package/src/EditorSelect.ts +99 -0
- package/src/EditorText.ts +92 -0
- package/src/FilterNumberRange.ts +89 -0
- package/src/FilterSelect.ts +68 -0
- package/src/FilterText.ts +85 -0
- package/src/GroupBar.ts +206 -0
- package/src/Popover.ts +446 -0
- package/src/index.ts +13 -0
- package/src/internal/middleware.ts +54 -0
- package/src/themes/base.css +102 -0
- package/src/themes/bootstrap.css +37 -0
- package/src/themes/material.css +35 -0
- package/src/themes/shadcn.css +41 -0
package/src/Popover.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing } from 'lit';
|
|
2
|
+
import { customElement, property, query, queryAssignedElements, state } from 'lit/decorators.js';
|
|
3
|
+
import { SignalWatcher, effect, untracked } from '@lit-labs/preact-signals';
|
|
4
|
+
import { attachOutsideClickListener, createLitControllableProperty, rozieAttr, rozieListeners, rozieSpread } from '@rozie/runtime-lit';
|
|
5
|
+
// The `offset` AND `arrow` middleware factories are ALIASED on import: both are
|
|
6
|
+
// ALSO author PROP names (`offset`, `arrow`). A bare `offset`/`arrow` shorthand in
|
|
7
|
+
// the buildMiddleware factories object resolves to the PROP — on Vue/Svelte the
|
|
8
|
+
// destructured prop local shadows the import, and on Angular the emitter rewrites
|
|
9
|
+
// the bare shorthand to the prop signal (`offset: this.offset()`, a number) instead
|
|
10
|
+
// of the middleware function (TS2322). Aliasing both severs the import↔prop clash.
|
|
11
|
+
// (The Cropper import-name==component-name class, applied to imports vs PROP names —
|
|
12
|
+
// two collisions, not one.) computePosition/autoUpdate/flip/shift carry no clash.
|
|
13
|
+
import { computePosition, autoUpdate, offset as offsetMiddleware, flip, shift, arrow as arrowMiddleware } from '@floating-ui/dom';
|
|
14
|
+
import { buildMiddleware } from './internal/middleware';
|
|
15
|
+
|
|
16
|
+
// null-lets so the bundled-leaf typeNeutralize pass annotates them `any`:
|
|
17
|
+
// anchorNode/floatingNode/arrowNode hold the resolved ref ELEMENTS (read ONLY in
|
|
18
|
+
// $onMount/handlers, ROZ123). They are deliberately named DIFFERENTLY from the
|
|
19
|
+
// `ref="anchorEl"` / `ref="floatingEl"` / `ref="arrowEl"` template ref names: the
|
|
20
|
+
// React/Svelte emitters declare a `const anchorEl = useRef(...)` for the ref, and a
|
|
21
|
+
// top-level `let anchorEl` hoisted to its own `useRef` would REDECLARE it (TS2451 —
|
|
22
|
+
// the local-name==ref-name self-shadow class, here in its `let X = null; X = $refs.X`
|
|
23
|
+
// variant, which deconflictRefShadows does NOT auto-rewrite since it only fires on the
|
|
24
|
+
// `const X = $refs.X` init shape).
|
|
25
|
+
// stopAutoUpdate is the autoUpdate teardown handle — a TOP-LEVEL `let` so the Solid
|
|
26
|
+
// onMount→onCleanup split (teardown is a separate closure) can still see it.
|
|
27
|
+
// lastFocusedEl (phase 72-06b) holds whatever had DOM focus at the moment a
|
|
28
|
+
// `trigger="click"` popover opened (natively the clicked trigger element itself,
|
|
29
|
+
// since a mousedown focuses a native `<button>` before its `click` fires) —
|
|
30
|
+
// restored on dismissal so Escape/click-outside don't drop focus to `<body>`.
|
|
31
|
+
// Same null-let convention as the others: read/written only in handlers, `any`
|
|
32
|
+
// via typeNeutralize.
|
|
33
|
+
|
|
34
|
+
interface RozieAnchorSlotCtx {
|
|
35
|
+
open: unknown;
|
|
36
|
+
toggle: unknown;
|
|
37
|
+
show: unknown;
|
|
38
|
+
hide: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@customElement('rozie-popover')
|
|
42
|
+
export default class Popover extends SignalWatcher(LitElement) {
|
|
43
|
+
static styles = css`
|
|
44
|
+
:host{display:contents}
|
|
45
|
+
.rozie-popover[data-rozie-s-c6cf02ea] {
|
|
46
|
+
display: contents;
|
|
47
|
+
}
|
|
48
|
+
.rozie-popover-anchor[data-rozie-s-c6cf02ea] {
|
|
49
|
+
display: inline-block;
|
|
50
|
+
}
|
|
51
|
+
.rozie-popover-floating[data-rozie-s-c6cf02ea] {
|
|
52
|
+
position: absolute;
|
|
53
|
+
left: 0;
|
|
54
|
+
top: 0;
|
|
55
|
+
z-index: var(--rozie-popover-z, 1000);
|
|
56
|
+
width: max-content;
|
|
57
|
+
max-width: var(--rozie-popover-max-width, calc(100vw - 16px));
|
|
58
|
+
background: var(--rozie-popover-bg, #fff);
|
|
59
|
+
color: var(--rozie-popover-color, inherit);
|
|
60
|
+
border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
|
|
61
|
+
border-radius: var(--rozie-popover-radius, 8px);
|
|
62
|
+
box-shadow: var(--rozie-popover-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
|
|
63
|
+
padding: var(--rozie-popover-padding, 8px 12px);
|
|
64
|
+
}
|
|
65
|
+
.rozie-popover-arrow[data-rozie-s-c6cf02ea] {
|
|
66
|
+
position: absolute;
|
|
67
|
+
width: var(--rozie-popover-arrow-size, 8px);
|
|
68
|
+
height: var(--rozie-popover-arrow-size, 8px);
|
|
69
|
+
background: var(--rozie-popover-bg, #fff);
|
|
70
|
+
border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
|
|
71
|
+
transform: rotate(45deg);
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default.
|
|
77
|
+
*/
|
|
78
|
+
@property({ type: Boolean, attribute: 'open' }) _open_attr: boolean = false;
|
|
79
|
+
private _openControllable = createLitControllableProperty<boolean>({ host: this, eventName: 'open-change', defaultValue: false, initialControlledValue: undefined });
|
|
80
|
+
/**
|
|
81
|
+
* Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime.
|
|
82
|
+
*/
|
|
83
|
+
@property({ type: String, reflect: true }) placement: string = 'bottom';
|
|
84
|
+
/**
|
|
85
|
+
* How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog).
|
|
86
|
+
*/
|
|
87
|
+
@property({ type: String, reflect: true }) trigger: string = 'click';
|
|
88
|
+
/**
|
|
89
|
+
* Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime.
|
|
90
|
+
*/
|
|
91
|
+
@property({ type: Number, reflect: true }) offset: number = 8;
|
|
92
|
+
/**
|
|
93
|
+
* Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless.
|
|
94
|
+
*/
|
|
95
|
+
@property({ type: Boolean, reflect: true }) disableFlip: boolean = false;
|
|
96
|
+
/**
|
|
97
|
+
* Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor.
|
|
98
|
+
*/
|
|
99
|
+
@property({ type: Boolean, reflect: true }) disableShift: boolean = false;
|
|
100
|
+
/**
|
|
101
|
+
* Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties.
|
|
102
|
+
*/
|
|
103
|
+
@property({ type: Boolean, reflect: true }) arrow: boolean = false;
|
|
104
|
+
/**
|
|
105
|
+
* Disable the control entirely: the trigger no longer opens the content and any open content is suppressed.
|
|
106
|
+
*/
|
|
107
|
+
@property({ type: Boolean, reflect: true }) disabled: boolean = false;
|
|
108
|
+
/**
|
|
109
|
+
* Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored).
|
|
110
|
+
*/
|
|
111
|
+
@property({ type: Boolean, reflect: true }) modal: boolean = false;
|
|
112
|
+
/**
|
|
113
|
+
* Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime.
|
|
114
|
+
*/
|
|
115
|
+
@property({ type: String, reflect: true }) strategy: string = 'absolute';
|
|
116
|
+
@query('[data-rozie-ref="anchorEl"]') private _refAnchorEl!: HTMLElement;
|
|
117
|
+
@query('[data-rozie-ref="floatingEl"]') private _refFloatingEl!: HTMLElement;
|
|
118
|
+
@query('[data-rozie-ref="arrowEl"]') private _refArrowEl!: HTMLElement;
|
|
119
|
+
private __rozieWatchInitial_0 = true;
|
|
120
|
+
private __rozieFirstUpdateDone = false;
|
|
121
|
+
|
|
122
|
+
@state() private _hasSlotAnchor = false;
|
|
123
|
+
@queryAssignedElements({ slot: 'anchor', flatten: true }) private _slotAnchorElements!: Element[];
|
|
124
|
+
@property({ attribute: false }) anchor?: (scope: { open: unknown; toggle: unknown; show: unknown; hide: unknown }) => unknown;
|
|
125
|
+
@state() private _hasSlotDefault = false;
|
|
126
|
+
@queryAssignedElements({ flatten: true }) private _slotDefaultElements!: Element[];
|
|
127
|
+
|
|
128
|
+
private _disconnectCleanups: Array<() => void> = [];
|
|
129
|
+
// Re-parenting guard: set true once the deferred teardown has actually
|
|
130
|
+
// run (a genuine un-mount), so a subsequent reconnect knows to re-arm.
|
|
131
|
+
private _rozieTornDown = false;
|
|
132
|
+
|
|
133
|
+
private _armListeners(): void {
|
|
134
|
+
const _lh0 = ($event: KeyboardEvent) => { if (!(this.open)) return; if ($event.key !== 'Escape') return; ((this.dismiss) as (...args: any[]) => any)($event); };
|
|
135
|
+
document.addEventListener('keydown', _lh0, undefined);
|
|
136
|
+
this._disconnectCleanups.push(() => document.removeEventListener('keydown', _lh0, undefined));
|
|
137
|
+
|
|
138
|
+
const _u1 = attachOutsideClickListener([() => this._refAnchorEl, () => this._refFloatingEl], ($event) => { ((this.dismiss) as (...args: any[]) => any)($event); }, () => (this.open));
|
|
139
|
+
this._disconnectCleanups.push(_u1);
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
const slotEl = this.shadowRoot?.querySelector('slot[name="anchor"]');
|
|
143
|
+
if (slotEl !== null && slotEl !== undefined) {
|
|
144
|
+
const update = () => { this._hasSlotAnchor = this._slotAnchorElements.length > 0; };
|
|
145
|
+
slotEl.addEventListener('slotchange', update);
|
|
146
|
+
// CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
|
|
147
|
+
this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
|
|
148
|
+
update();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
const slotEl = this.shadowRoot?.querySelector('slot:not([name])');
|
|
154
|
+
if (slotEl !== null && slotEl !== undefined) {
|
|
155
|
+
const update = () => { this._hasSlotDefault = this._slotDefaultElements.length > 0; };
|
|
156
|
+
slotEl.addEventListener('slotchange', update);
|
|
157
|
+
// CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
|
|
158
|
+
this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
|
|
159
|
+
update();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
connectedCallback(): void {
|
|
165
|
+
// Phase 07.3.1 D-LIT-15 — pre-seed _hasSlot<X> from light DOM so first render isn't deadlocked.
|
|
166
|
+
this._hasSlotAnchor = Array.from(this.children).some((el) => el.getAttribute('slot') === 'anchor');
|
|
167
|
+
this._hasSlotDefault = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
|
|
168
|
+
super.connectedCallback();
|
|
169
|
+
if (this.hasUpdated && this._rozieTornDown) { this._rozieTornDown = false; this._armListeners(); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
firstUpdated(): void {
|
|
173
|
+
this._armListeners();
|
|
174
|
+
|
|
175
|
+
this._disconnectCleanups.push((() => {
|
|
176
|
+
this.stopTracking();
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
this._disconnectCleanups.push(effect(() => { const __watchVal = (() => this.open)(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } ((isOpen: any) => {
|
|
180
|
+
if (isOpen && !this.disabled) {
|
|
181
|
+
queueMicrotask(() => {
|
|
182
|
+
if (!this.open || this.disabled) return;
|
|
183
|
+
this.floatingNode = this._refFloatingEl;
|
|
184
|
+
this.arrowNode = this._refArrowEl;
|
|
185
|
+
this.startTracking();
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
this.stopTracking();
|
|
189
|
+
}
|
|
190
|
+
})(__watchVal); }); }));
|
|
191
|
+
|
|
192
|
+
// $refs read ONLY here (ROZ123). The floating + arrow elements live behind r-if
|
|
193
|
+
// and may be null until open; startTracking re-reads via the watch path.
|
|
194
|
+
this.anchorNode = this._refAnchorEl;
|
|
195
|
+
if (this.open && !this.disabled) {
|
|
196
|
+
// floatingNode is populated by its r-if having rendered; read it lazily inside
|
|
197
|
+
// the watch/handlers too. Position on next tick when it exists.
|
|
198
|
+
this.floatingNode = this._refFloatingEl;
|
|
199
|
+
this.arrowNode = this._refArrowEl;
|
|
200
|
+
this.startTracking();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updated(changedProperties: Map<string, unknown>): void {
|
|
205
|
+
if (this.__rozieFirstUpdateDone && (changedProperties.has('placement'))) { const __watchVal = (() => this.placement)(); (() => {
|
|
206
|
+
if (this.open) this.position();
|
|
207
|
+
})(); }
|
|
208
|
+
if (this.__rozieFirstUpdateDone && (changedProperties.has('offset'))) { const __watchVal = (() => this.offset)(); (() => {
|
|
209
|
+
if (this.open) this.position();
|
|
210
|
+
})(); }
|
|
211
|
+
if (this.__rozieFirstUpdateDone && (changedProperties.has('disableFlip'))) { const __watchVal = (() => this.disableFlip)(); (() => {
|
|
212
|
+
if (this.open) this.position();
|
|
213
|
+
})(); }
|
|
214
|
+
if (this.__rozieFirstUpdateDone && (changedProperties.has('disableShift'))) { const __watchVal = (() => this.disableShift)(); (() => {
|
|
215
|
+
if (this.open) this.position();
|
|
216
|
+
})(); }
|
|
217
|
+
if (this.__rozieFirstUpdateDone && (changedProperties.has('strategy'))) { const __watchVal = (() => this.strategy)(); (() => {
|
|
218
|
+
if (this.open) this.position();
|
|
219
|
+
})(); }
|
|
220
|
+
this.__rozieFirstUpdateDone = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
disconnectedCallback(): void {
|
|
224
|
+
super.disconnectedCallback();
|
|
225
|
+
queueMicrotask(() => {
|
|
226
|
+
if (this.isConnected || this._rozieTornDown) return;
|
|
227
|
+
this._rozieTornDown = true;
|
|
228
|
+
for (const fn of this._disconnectCleanups) fn();
|
|
229
|
+
this._disconnectCleanups = [];
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
attributeChangedCallback(name: string, old: string | null, value: string | null): void {
|
|
234
|
+
super.attributeChangedCallback(name, old, value);
|
|
235
|
+
if (name === 'open') this._openControllable.notifyAttributeChange(value !== null);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
render() {
|
|
239
|
+
return html`
|
|
240
|
+
<div class="rozie-popover" ${rozieSpread(this.$attrs)} ${rozieListeners(this.$listeners)} data-rozie-s-c6cf02ea>
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
<div class="rozie-popover-anchor" aria-haspopup="dialog" aria-expanded=${!!this.open} aria-describedby=${rozieAttr(this.isTooltip() && this.open ? 'rozie-popover-floating' : null)} @click=${($event: MouseEvent & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.trigger === 'click' && this.onAnchorClick(); }} @pointerenter=${($event: Event & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.trigger === 'hover' && this.onAnchorPointerEnter(); }} @pointerleave=${($event: Event & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.trigger === 'hover' && this.onAnchorPointerLeave(); }} @focusin=${($event: Event & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.trigger === 'focus' && this.onAnchorFocus(); }} @focusout=${($event: Event & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.trigger === 'focus' && this.onAnchorBlur(); }} data-rozie-ref="anchorEl" data-rozie-s-c6cf02ea>
|
|
244
|
+
${this.anchor !== undefined ? this.anchor({open: this.open, toggle: this.toggle, show: this.show, hide: this.hide}) : html`<slot name="anchor" data-rozie-params=${(() => { try { return JSON.stringify({open: this.open}); } catch { return '{}'; } })()} @rozie-anchor-toggle=${($event: CustomEvent) => ((this.toggle) as (...args: any[]) => any)($event.detail)} @rozie-anchor-show=${($event: CustomEvent) => ((this.show) as (...args: any[]) => any)($event.detail)} @rozie-anchor-hide=${($event: CustomEvent) => ((this.hide) as (...args: any[]) => any)($event.detail)}></slot>`}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
${this.open && !this.disabled ? html`<div class="rozie-popover-floating" id="rozie-popover-floating" role=${rozieAttr(this.floatingRole())} aria-modal=${!!(this.floatingRole() === 'dialog')} data-rozie-ref="floatingEl" data-rozie-s-c6cf02ea>
|
|
249
|
+
${this.arrow ? html`<div class="rozie-popover-arrow" data-rozie-ref="arrowEl" data-rozie-s-c6cf02ea></div>` : nothing}<slot></slot>
|
|
250
|
+
</div>` : nothing}</div>
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
anchorNode: any = null;
|
|
255
|
+
|
|
256
|
+
floatingNode: any = null;
|
|
257
|
+
|
|
258
|
+
arrowNode: any = null;
|
|
259
|
+
|
|
260
|
+
stopAutoUpdate: any = null;
|
|
261
|
+
|
|
262
|
+
lastFocusedEl: any = null;
|
|
263
|
+
|
|
264
|
+
deepActiveElement = () => {
|
|
265
|
+
let el = document.activeElement;
|
|
266
|
+
while (el && el.shadowRoot && el.shadowRoot.activeElement) {
|
|
267
|
+
el = el.shadowRoot.activeElement;
|
|
268
|
+
}
|
|
269
|
+
return el;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
requestOpen = (next: any) => {
|
|
273
|
+
if (this.open === next) return;
|
|
274
|
+
if (next && this.trigger === 'click') {
|
|
275
|
+
this.lastFocusedEl = this.deepActiveElement();
|
|
276
|
+
}
|
|
277
|
+
this._openControllable.write(next);
|
|
278
|
+
this.dispatchEvent(new CustomEvent("change", {
|
|
279
|
+
detail: next,
|
|
280
|
+
bubbles: true,
|
|
281
|
+
composed: true
|
|
282
|
+
}));
|
|
283
|
+
if (!next && this.trigger === 'click' && this.lastFocusedEl && this.lastFocusedEl.isConnected && typeof this.lastFocusedEl.focus === 'function') {
|
|
284
|
+
this.lastFocusedEl.focus();
|
|
285
|
+
}
|
|
286
|
+
if (!next) {
|
|
287
|
+
this.lastFocusedEl = null;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
applyPosition = (x: any, y: any, middlewareData: any) => {
|
|
292
|
+
if (!this.floatingNode) return;
|
|
293
|
+
this.floatingNode.style.left = x + 'px';
|
|
294
|
+
this.floatingNode.style.top = y + 'px';
|
|
295
|
+
if (this.arrowNode && middlewareData && middlewareData.arrow) {
|
|
296
|
+
const ax = middlewareData.arrow.x;
|
|
297
|
+
const ay = middlewareData.arrow.y;
|
|
298
|
+
this.arrowNode.style.left = ax == null ? '' : ax + 'px';
|
|
299
|
+
this.arrowNode.style.top = ay == null ? '' : ay + 'px';
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
position = () => {
|
|
304
|
+
if (!this.anchorNode || !this.floatingNode) return;
|
|
305
|
+
const middleware = buildMiddleware({
|
|
306
|
+
offset: offsetMiddleware,
|
|
307
|
+
flip,
|
|
308
|
+
shift,
|
|
309
|
+
arrow: arrowMiddleware
|
|
310
|
+
}, {
|
|
311
|
+
offset: this.offset,
|
|
312
|
+
disableFlip: this.disableFlip,
|
|
313
|
+
disableShift: this.disableShift,
|
|
314
|
+
arrow: this.arrow,
|
|
315
|
+
arrowEl: this.arrowNode
|
|
316
|
+
});
|
|
317
|
+
// 'fixed' inline position MUST be written before computePosition measures the
|
|
318
|
+
// floating element's offset parent (fixed vs absolute changes the containing
|
|
319
|
+
// block). Default 'absolute' explicitly CLEARS any inline position instead of
|
|
320
|
+
// writing `position: absolute` — so a never-fixed popover still writes no
|
|
321
|
+
// visible inline position (byte-identical-off preserved: `style.position = ''`
|
|
322
|
+
// is a no-op when the property was never set), while a live `strategy`
|
|
323
|
+
// reconcile (fixed → absolute, see the $watch below) correctly resets the
|
|
324
|
+
// stale inline `fixed` so the stylesheet's `position: absolute` rule re-takes
|
|
325
|
+
// over instead of positioning `fixed` with absolute-computed coordinates
|
|
326
|
+
// (72-REVIEW.md WR-01).
|
|
327
|
+
if (this.strategy === 'fixed') {
|
|
328
|
+
this.floatingNode.style.position = 'fixed';
|
|
329
|
+
} else {
|
|
330
|
+
this.floatingNode.style.position = '';
|
|
331
|
+
}
|
|
332
|
+
let opts: any = null;
|
|
333
|
+
opts = {
|
|
334
|
+
placement: this.placement,
|
|
335
|
+
strategy: this.strategy,
|
|
336
|
+
middleware
|
|
337
|
+
};
|
|
338
|
+
computePosition(this.anchorNode, this.floatingNode, opts).then((result: any) => {
|
|
339
|
+
this.applyPosition(result.x, result.y, result.middlewareData);
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
startTracking = () => {
|
|
344
|
+
if (!this.anchorNode || !this.floatingNode) return;
|
|
345
|
+
if (this.stopAutoUpdate) {
|
|
346
|
+
this.stopAutoUpdate();
|
|
347
|
+
this.stopAutoUpdate = null;
|
|
348
|
+
}
|
|
349
|
+
this.stopAutoUpdate = autoUpdate(this.anchorNode, this.floatingNode, this.position);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
stopTracking = () => {
|
|
353
|
+
if (this.stopAutoUpdate) {
|
|
354
|
+
this.stopAutoUpdate();
|
|
355
|
+
this.stopAutoUpdate = null;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
onAnchorClick = () => {
|
|
360
|
+
if (this.disabled) return;
|
|
361
|
+
this.requestOpen(!this.open);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
onAnchorPointerEnter = () => {
|
|
365
|
+
if (this.disabled) return;
|
|
366
|
+
this.requestOpen(true);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
onAnchorPointerLeave = () => {
|
|
370
|
+
if (this.disabled) return;
|
|
371
|
+
this.requestOpen(false);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
onAnchorFocus = () => {
|
|
375
|
+
if (this.disabled) return;
|
|
376
|
+
this.requestOpen(true);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
onAnchorBlur = () => {
|
|
380
|
+
if (this.disabled) return;
|
|
381
|
+
this.requestOpen(false);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
dismiss = () => {
|
|
385
|
+
this.requestOpen(false);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
isTooltip = () => this.trigger === 'hover' || this.trigger === 'focus';
|
|
389
|
+
|
|
390
|
+
floatingRole = () => this.isTooltip() ? 'tooltip' : this.modal ? 'dialog' : undefined;
|
|
391
|
+
|
|
392
|
+
show() {
|
|
393
|
+
if (!this.disabled) this.requestOpen(true);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
hide() {
|
|
397
|
+
this.requestOpen(false);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
toggle() {
|
|
401
|
+
if (!this.disabled) this.requestOpen(!this.open);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
reposition() {
|
|
405
|
+
this.position();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
get open(): boolean { return this._openControllable.read(); }
|
|
409
|
+
set open(v: boolean) { this._openControllable.notifyPropertyWrite(v); }
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Plan 14-05 — cross-framework attribute fallthrough source. Reads the
|
|
413
|
+
* host custom element's attributes on each call so a consumer-side bound
|
|
414
|
+
* attribute flows through on every render. The `rozieSpread` directive
|
|
415
|
+
* (D-02) does the cross-render diff downstream.
|
|
416
|
+
*
|
|
417
|
+
* Phase 15 follow-up Bug A — declared-prop attribute names are filtered
|
|
418
|
+
* out so `$attrs` returns "rest after declared props" (semantic parity
|
|
419
|
+
* with React/Vue/Svelte/Solid/Angular). Both Lit attribute-naming
|
|
420
|
+
* forms are folded into the skip set: kebab-case for model props
|
|
421
|
+
* (explicit `attribute:`) AND lowercased property name (Lit's default).
|
|
422
|
+
*/
|
|
423
|
+
private get $attrs(): Record<string, string> {
|
|
424
|
+
const __skip = new Set<string>(['open', 'placement', 'trigger', 'offset', 'disable-flip', 'disableflip', 'disable-shift', 'disableshift', 'arrow', 'disabled', 'modal', 'strategy']);
|
|
425
|
+
const out: Record<string, string> = {};
|
|
426
|
+
for (const a of Array.from(this.attributes)) {
|
|
427
|
+
if (__skip.has(a.name)) continue;
|
|
428
|
+
out[a.name] = a.value;
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Phase 15 D-19 — consumer-passed listener cluster placeholder.
|
|
435
|
+
* Lit attaches event listeners directly on the host element via
|
|
436
|
+
* `addEventListener` (no per-instance prop rest binding), so the
|
|
437
|
+
* runtime value is undefined; the `rozieListeners` directive's
|
|
438
|
+
* nullish coercion (`obj ?? {}`) handles the no-op cleanly.
|
|
439
|
+
* The declaration exists to satisfy `tsc --noEmit` on consumer
|
|
440
|
+
* projects with strict mode — bare `$listeners` in `render()`
|
|
441
|
+
* would otherwise raise TS2304 (Cannot find name).
|
|
442
|
+
*/
|
|
443
|
+
private get $listeners(): Record<string, EventListener> | undefined {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as DataTable } from './DataTable';
|
|
2
|
+
export { default } from './DataTable';
|
|
3
|
+
export { default as Column } from './Column';
|
|
4
|
+
export { default as EditorText } from './EditorText';
|
|
5
|
+
export { default as EditorNumber } from './EditorNumber';
|
|
6
|
+
export { default as EditorSelect } from './EditorSelect';
|
|
7
|
+
export { default as EditorCheckbox } from './EditorCheckbox';
|
|
8
|
+
export { default as EditorDate } from './EditorDate';
|
|
9
|
+
export { default as FilterText } from './FilterText';
|
|
10
|
+
export { default as FilterNumberRange } from './FilterNumberRange';
|
|
11
|
+
export { default as FilterSelect } from './FilterSelect';
|
|
12
|
+
export { default as GroupBar } from './GroupBar';
|
|
13
|
+
export { default as DetailPanel } from './DetailPanel';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* middleware.ts — pure construction of the Floating UI middleware stack from the
|
|
3
|
+
* Popover's first-class props.
|
|
4
|
+
*
|
|
5
|
+
* This is the one piece of BRANCHY engine glue in Popover.rozie (offset always on,
|
|
6
|
+
* flip/shift opt-out via `disableFlip`/`disableShift`, arrow opt-in via `arrow` +
|
|
7
|
+
* an element). Extracted to `src/internal/` so it can be unit-tested in isolation
|
|
8
|
+
* (codegen vendors `src/internal/` into every leaf via copyInternal, excluding
|
|
9
|
+
* `*.test.ts`).
|
|
10
|
+
*
|
|
11
|
+
* It takes the Floating UI middleware FACTORIES as arguments (rather than importing
|
|
12
|
+
* `@floating-ui/dom` itself) so the unit test can pass lightweight stand-ins and
|
|
13
|
+
* assert ORDER + presence without pulling the real engine — and so the vendored
|
|
14
|
+
* copy in each leaf has zero engine import of its own (the leaf's Popover.* owns
|
|
15
|
+
* the single engine import).
|
|
16
|
+
*
|
|
17
|
+
* Floating UI ordering contract: offset → flip → shift → arrow. `offset` first so
|
|
18
|
+
* the gap is measured before collision detection; `arrow` last so it reads the
|
|
19
|
+
* final resolved placement.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface MiddlewareFactories {
|
|
23
|
+
offset: (value: number) => unknown;
|
|
24
|
+
flip: () => unknown;
|
|
25
|
+
shift: () => unknown;
|
|
26
|
+
arrow: (opts: { element: Element }) => unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MiddlewareConfig {
|
|
30
|
+
offset: number;
|
|
31
|
+
disableFlip: boolean;
|
|
32
|
+
disableShift: boolean;
|
|
33
|
+
arrow: boolean;
|
|
34
|
+
arrowEl: Element | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the ordered middleware array for `computePosition`. Pure — no engine
|
|
39
|
+
* import, no DOM read beyond the passed-in arrow element.
|
|
40
|
+
*/
|
|
41
|
+
export function buildMiddleware(
|
|
42
|
+
factories: MiddlewareFactories,
|
|
43
|
+
config: MiddlewareConfig,
|
|
44
|
+
): unknown[] {
|
|
45
|
+
const mw: unknown[] = [factories.offset(config.offset)];
|
|
46
|
+
if (!config.disableFlip) mw.push(factories.flip());
|
|
47
|
+
if (!config.disableShift) mw.push(factories.shift());
|
|
48
|
+
// The arrow middleware needs a real element to position; opt-in only when both
|
|
49
|
+
// the `arrow` prop is set AND the arrow element has mounted.
|
|
50
|
+
if (config.arrow && config.arrowEl) {
|
|
51
|
+
mw.push(factories.arrow({ element: config.arrowEl }));
|
|
52
|
+
}
|
|
53
|
+
return mw;
|
|
54
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @rozie-ui/data-table — base token reference.
|
|
3
|
+
*
|
|
4
|
+
* Every COSMETIC value the table renders flows through a `--rozie-data-table-*`
|
|
5
|
+
* PUBLIC token. The component's scoped `<style>` reads short internal `--rdt-*`
|
|
6
|
+
* tokens with inline `var(--rdt-x, <fallback>)` defaults, so the table renders
|
|
7
|
+
* ZERO-CONFIG without importing this file — it already paints a sensible default.
|
|
8
|
+
* Importing this file simply makes the full public token surface explicit in one
|
|
9
|
+
* place and wires each public `--rozie-data-table-*` token onto the internal
|
|
10
|
+
* `--rdt-*` the component consumes, so a design-system bridge (shadcn / material /
|
|
11
|
+
* bootstrap) can re-skin the whole table by remapping ONLY the public tokens.
|
|
12
|
+
*
|
|
13
|
+
* NOTE the split (CONTEXT "Claude's Discretion" + the slider D-08 precedent): the
|
|
14
|
+
* STRUCTURAL rules — `border-collapse`, the keyed-r-for layout, `position: sticky`
|
|
15
|
+
* for the header + pinned columns, the resize-handle positioning, the
|
|
16
|
+
* `<details>` disclosure box — are behavior-critical and live in the component's
|
|
17
|
+
* scoped `<style>` (per-leaf, NOT consumer-overridable). Only the values below
|
|
18
|
+
* flow through tokens.
|
|
19
|
+
*
|
|
20
|
+
* Override any token at any ancestor scope (`:root`, `.dark`, a wrapper, or the
|
|
21
|
+
* `.rozie-data-table` / `.rozie-data-table-wrap` element). See
|
|
22
|
+
* themes/shadcn.css, themes/material.css and themes/bootstrap.css for the
|
|
23
|
+
* design-system bridges.
|
|
24
|
+
*/
|
|
25
|
+
.rozie-data-table-wrap,
|
|
26
|
+
.rozie-data-table {
|
|
27
|
+
/* ── palette (the few values most consumers will set) ───────────────────── */
|
|
28
|
+
--rozie-data-table-fg: inherit;
|
|
29
|
+
--rozie-data-table-accent: #0066cc;
|
|
30
|
+
--rozie-data-table-border-color: rgba(0, 0, 0, 0.08);
|
|
31
|
+
--rozie-data-table-control-border-color: rgba(0, 0, 0, 0.2);
|
|
32
|
+
--rozie-data-table-radius: 4px;
|
|
33
|
+
|
|
34
|
+
/* ── typography / cell metrics ──────────────────────────────────────────── */
|
|
35
|
+
--rozie-data-table-font: 14px system-ui, sans-serif;
|
|
36
|
+
--rozie-data-table-cell-padding: 0.5rem 0.75rem;
|
|
37
|
+
|
|
38
|
+
/* ── header ─────────────────────────────────────────────────────────────── */
|
|
39
|
+
/* OPAQUE (was rgba(0,0,0,0.03)): a translucent header lets the scrolling body bleed
|
|
40
|
+
through in sticky mode. #f7f7f7 ≈ the old 3%-black tint over white, but solid —
|
|
41
|
+
matching the opaque header backgrounds the design-system themes already ship. */
|
|
42
|
+
--rozie-data-table-header-bg: #f7f7f7;
|
|
43
|
+
--rozie-data-table-header-weight: 600;
|
|
44
|
+
|
|
45
|
+
/* ── sort indicator ─────────────────────────────────────────────────────── */
|
|
46
|
+
--rozie-data-table-sort-indicator-opacity: 0.7;
|
|
47
|
+
|
|
48
|
+
/* ── resize handle (req-9) ──────────────────────────────────────────────── */
|
|
49
|
+
--rozie-data-table-resize-grip-color: rgba(0, 0, 0, 0.12);
|
|
50
|
+
--rozie-data-table-resize-grip-active: var(--rozie-data-table-accent);
|
|
51
|
+
|
|
52
|
+
/* ── pin controls (req-11) ──────────────────────────────────────────────── */
|
|
53
|
+
--rozie-data-table-pin-active-bg: rgba(0, 0, 0, 0.1);
|
|
54
|
+
|
|
55
|
+
/* ── column-visibility menu (req-8) ─────────────────────────────────────── */
|
|
56
|
+
--rozie-data-table-menu-bg: #fff;
|
|
57
|
+
--rozie-data-table-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
58
|
+
|
|
59
|
+
/* ── selection checkboxes (req-7) ───────────────────────────────────────── */
|
|
60
|
+
--rozie-data-table-select-accent: var(--rozie-data-table-accent);
|
|
61
|
+
|
|
62
|
+
/* ── disabled pagination controls ──────────────────────────────────────── */
|
|
63
|
+
--rozie-data-table-disabled-opacity: 0.4;
|
|
64
|
+
|
|
65
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
66
|
+
* Wire the PUBLIC tokens onto the INTERNAL `--rdt-*` the component reads. A
|
|
67
|
+
* bridge only needs to remap the public tokens above; these passthroughs do
|
|
68
|
+
* the rest. (The component still renders zero-config without this file via its
|
|
69
|
+
* own inline `var(--rdt-x, <fallback>)` defaults.)
|
|
70
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
71
|
+
--rdt-font: var(--rozie-data-table-font);
|
|
72
|
+
--rdt-color: var(--rozie-data-table-fg);
|
|
73
|
+
--rdt-cell-padding: var(--rozie-data-table-cell-padding);
|
|
74
|
+
--rdt-border: 1px solid var(--rozie-data-table-border-color);
|
|
75
|
+
--rdt-header-bg: var(--rozie-data-table-header-bg);
|
|
76
|
+
--rdt-header-weight: var(--rozie-data-table-header-weight);
|
|
77
|
+
--rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity);
|
|
78
|
+
|
|
79
|
+
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
80
|
+
--rdt-filter-radius: var(--rozie-data-table-radius);
|
|
81
|
+
--rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
82
|
+
--rdt-page-btn-radius: var(--rozie-data-table-radius);
|
|
83
|
+
--rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity);
|
|
84
|
+
--rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
85
|
+
--rdt-page-size-radius: var(--rozie-data-table-radius);
|
|
86
|
+
|
|
87
|
+
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color);
|
|
88
|
+
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active);
|
|
89
|
+
|
|
90
|
+
--rdt-pin-btn-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
91
|
+
--rdt-pin-btn-radius: var(--rozie-data-table-radius);
|
|
92
|
+
--rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg);
|
|
93
|
+
|
|
94
|
+
--rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color);
|
|
95
|
+
--rdt-colvis-summary-radius: var(--rozie-data-table-radius);
|
|
96
|
+
--rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color);
|
|
97
|
+
--rdt-colvis-menu-radius: var(--rozie-data-table-radius);
|
|
98
|
+
--rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg);
|
|
99
|
+
--rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow);
|
|
100
|
+
|
|
101
|
+
--rdt-select-accent: var(--rozie-data-table-select-accent);
|
|
102
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @rozie-ui/data-table — Bootstrap 5 bridge.
|
|
3
|
+
*
|
|
4
|
+
* Maps the public `--rozie-data-table-*` tokens onto Bootstrap's `--bs-*` CSS
|
|
5
|
+
* variables (Bootstrap 5.2+). Reading them live means the table follows
|
|
6
|
+
* `data-bs-theme="dark"` and any custom `$primary` build automatically. Falls
|
|
7
|
+
* back to Bootstrap's default light palette when the vars are absent.
|
|
8
|
+
*
|
|
9
|
+
* Import this AFTER base.css (base.css wires the public tokens onto the internal
|
|
10
|
+
* `--rdt-*` the component reads):
|
|
11
|
+
*
|
|
12
|
+
* import '@rozie-ui/data-table-react/themes/base.css';
|
|
13
|
+
* import '@rozie-ui/data-table-react/themes/bootstrap.css';
|
|
14
|
+
*/
|
|
15
|
+
.rozie-data-table-wrap,
|
|
16
|
+
.rozie-data-table {
|
|
17
|
+
--rozie-data-table-fg: var(--bs-body-color, #212529);
|
|
18
|
+
--rozie-data-table-accent: var(--bs-primary, #0d6efd);
|
|
19
|
+
--rozie-data-table-border-color: var(--bs-border-color, #dee2e6);
|
|
20
|
+
--rozie-data-table-control-border-color: var(--bs-border-color, #dee2e6);
|
|
21
|
+
--rozie-data-table-radius: var(--bs-border-radius, 0.375rem);
|
|
22
|
+
|
|
23
|
+
--rozie-data-table-header-bg: var(--bs-tertiary-bg, #f8f9fa);
|
|
24
|
+
--rozie-data-table-header-weight: 600;
|
|
25
|
+
|
|
26
|
+
--rozie-data-table-resize-grip-color: var(--bs-border-color, #dee2e6);
|
|
27
|
+
--rozie-data-table-resize-grip-active: var(--bs-primary, #0d6efd);
|
|
28
|
+
|
|
29
|
+
--rozie-data-table-pin-active-bg: var(--bs-secondary-bg, #e9ecef);
|
|
30
|
+
|
|
31
|
+
--rozie-data-table-menu-bg: var(--bs-body-bg, #fff);
|
|
32
|
+
--rozie-data-table-menu-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
33
|
+
|
|
34
|
+
--rozie-data-table-disabled-opacity: 0.5;
|
|
35
|
+
|
|
36
|
+
--rozie-data-table-select-accent: var(--bs-primary, #0d6efd);
|
|
37
|
+
}
|