@unofficialbox/box-open-elements 0.2.1 → 0.4.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.
Files changed (87) hide show
  1. package/dist/components/actions/button.d.ts +15 -0
  2. package/dist/components/actions/button.js +107 -3
  3. package/dist/components/actions/link-button.d.ts +9 -0
  4. package/dist/components/actions/link-button.js +55 -4
  5. package/dist/components/actions/menu.d.ts +10 -0
  6. package/dist/components/actions/menu.js +68 -11
  7. package/dist/components/collections/carousel.d.ts +7 -0
  8. package/dist/components/collections/carousel.js +71 -4
  9. package/dist/components/collections/datalist-item.d.ts +6 -0
  10. package/dist/components/collections/datalist-item.js +39 -1
  11. package/dist/components/collections/draggable-list.js +1 -1
  12. package/dist/components/collections/grid-view.js +7 -3
  13. package/dist/components/collections/table.d.ts +53 -0
  14. package/dist/components/collections/table.js +323 -0
  15. package/dist/components/collections/thumbnail-card.d.ts +19 -0
  16. package/dist/components/collections/thumbnail-card.js +210 -0
  17. package/dist/components/feedback/alert.d.ts +2 -0
  18. package/dist/components/feedback/alert.js +30 -1
  19. package/dist/components/feedback/badge.d.ts +10 -0
  20. package/dist/components/feedback/badge.js +70 -4
  21. package/dist/components/feedback/badgeable.d.ts +14 -0
  22. package/dist/components/feedback/badgeable.js +84 -0
  23. package/dist/components/feedback/chip.d.ts +5 -0
  24. package/dist/components/feedback/chip.js +74 -2
  25. package/dist/components/feedback/error-mask.d.ts +2 -0
  26. package/dist/components/feedback/error-mask.js +23 -0
  27. package/dist/components/feedback/help-text.js +7 -0
  28. package/dist/components/feedback/spinner.d.ts +4 -0
  29. package/dist/components/feedback/spinner.js +30 -2
  30. package/dist/components/feedback/toast.d.ts +5 -0
  31. package/dist/components/feedback/toast.js +59 -11
  32. package/dist/components/forms/category-selector.d.ts +11 -0
  33. package/dist/components/forms/category-selector.js +211 -17
  34. package/dist/components/forms/checkbox.d.ts +6 -0
  35. package/dist/components/forms/checkbox.js +58 -1
  36. package/dist/components/forms/combobox.d.ts +30 -1
  37. package/dist/components/forms/combobox.js +348 -44
  38. package/dist/components/forms/date-field.d.ts +8 -0
  39. package/dist/components/forms/date-field.js +80 -3
  40. package/dist/components/forms/dropdown.d.ts +5 -0
  41. package/dist/components/forms/dropdown.js +32 -3
  42. package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
  43. package/dist/components/forms/pill-selector-dropdown.js +127 -0
  44. package/dist/components/forms/radio-group.d.ts +9 -8
  45. package/dist/components/forms/radio-group.js +43 -6
  46. package/dist/components/forms/search-field.d.ts +9 -0
  47. package/dist/components/forms/search-field.js +62 -15
  48. package/dist/components/forms/select.d.ts +19 -8
  49. package/dist/components/forms/select.js +110 -15
  50. package/dist/components/forms/text-field.d.ts +14 -0
  51. package/dist/components/forms/text-field.js +144 -3
  52. package/dist/components/forms/time-field.d.ts +11 -0
  53. package/dist/components/forms/time-field.js +51 -0
  54. package/dist/components/identity/avatar.d.ts +4 -0
  55. package/dist/components/identity/avatar.js +65 -1
  56. package/dist/components/identity/contact-datalist-item.d.ts +8 -0
  57. package/dist/components/identity/contact-datalist-item.js +71 -2
  58. package/dist/components/layout/nav-sidebar.js +23 -0
  59. package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
  60. package/dist/components/layout/sidebar-toggle-button.js +21 -2
  61. package/dist/components/navigation/accordion.d.ts +3 -0
  62. package/dist/components/navigation/accordion.js +17 -2
  63. package/dist/components/navigation/breadcrumb.d.ts +30 -0
  64. package/dist/components/navigation/breadcrumb.js +190 -0
  65. package/dist/components/navigation/tabs.d.ts +4 -0
  66. package/dist/components/navigation/tabs.js +53 -5
  67. package/dist/components/overlays/context-menu.d.ts +42 -0
  68. package/dist/components/overlays/context-menu.js +260 -0
  69. package/dist/components/overlays/dialog.d.ts +8 -0
  70. package/dist/components/overlays/dialog.js +45 -2
  71. package/dist/components/overlays/guide-tooltip.d.ts +51 -0
  72. package/dist/components/overlays/guide-tooltip.js +361 -0
  73. package/dist/components/overlays/popover.d.ts +9 -1
  74. package/dist/components/overlays/popover.js +37 -22
  75. package/dist/components/overlays/tooltip.d.ts +10 -0
  76. package/dist/components/overlays/tooltip.js +74 -11
  77. package/dist/core/form-associated.d.ts +32 -0
  78. package/dist/core/form-associated.js +114 -0
  79. package/dist/core/index.d.ts +1 -1
  80. package/dist/core/index.js +1 -1
  81. package/dist/foundations/overlay/index.d.ts +2 -0
  82. package/dist/foundations/overlay/index.js +1 -0
  83. package/dist/foundations/overlay/positioning.d.ts +74 -0
  84. package/dist/foundations/overlay/positioning.js +162 -0
  85. package/dist/index.d.ts +8 -0
  86. package/dist/index.js +8 -0
  87. package/package.json +1 -1
@@ -29,10 +29,26 @@ export declare const rangeFromFormValue: (value: FormValue, name: string, fallba
29
29
  export declare const boeFormFieldErrorStyles = "\n :host([invalid]) [part=\"input\"],\n :host([invalid]) [part=\"textarea\"],\n :host([invalid]) [part=\"select\"],\n :host([invalid]) [part=\"control\"],\n :host([invalid]) [part=\"editor\"] {\n border-color: var(--boe-token-surface-status-surface-error, #ed3757);\n }\n\n :host([invalid]) [part=\"input\"]:focus-visible,\n :host([invalid]) [part=\"textarea\"]:focus-visible,\n :host([invalid]) [part=\"select\"]:focus-visible,\n :host([invalid]) [part=\"control\"]:focus-visible {\n border-color: var(--boe-token-surface-status-surface-error, #ed3757);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 22%, transparent);\n }\n\n [part=\"error-message\"] {\n margin: 0;\n color: var(--boe-token-surface-status-surface-error, #ed3757);\n font-size: 0.82rem;\n line-height: 1.35;\n }\n\n [part=\"error-message\"][hidden] {\n display: none;\n }\n";
30
30
  export declare const FORM_ERROR_MESSAGE_ID = "boe-field-error";
31
31
  export declare const formErrorMessageMarkup: () => string;
32
+ export declare const FORM_DESCRIPTION_ID = "boe-field-description";
33
+ /** Optional help text rendered under the label (shown only when `description` is set). */
34
+ export declare const formDescriptionMarkup: () => string;
35
+ /**
36
+ * Shared chrome for the field-feature set: help-text `description`, the
37
+ * `required` indicator, and the visually-hidden `hide-label` treatment.
38
+ * Include alongside `boeFormFieldErrorStyles` in a field's stylesheet.
39
+ */
40
+ export declare const boeFormFieldSupportStyles = "\n [part=\"description\"] {\n margin: 0;\n color: var(--boe-token-text-text-secondary, #6f6f6f);\n font-size: 0.82rem;\n line-height: 1.35;\n }\n\n [part=\"description\"][hidden] {\n display: none;\n }\n\n [part=\"label\"] .boe-required-mark {\n margin-inline-start: 2px;\n color: var(--boe-token-surface-status-surface-error, #ed3757);\n }\n\n :host([hide-label]) [part=\"label\"] {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n";
32
41
  export declare abstract class FormAssociatedElement extends BaseElement {
33
42
  static formAssociated: boolean;
34
43
  /** Attributes every form control should observe (merge into subclass lists). */
35
44
  static readonly formObservedAttributes: readonly ["name", "invalid", "error-message"];
45
+ /**
46
+ * `formObservedAttributes` plus the shared field-feature attributes
47
+ * (`required`, `description`, `hide-label`). A field that renders a label +
48
+ * `formDescriptionMarkup()` and calls `applyFieldSupport()` should observe
49
+ * these instead.
50
+ */
51
+ static readonly fieldObservedAttributes: readonly ["name", "invalid", "error-message", "required", "description", "hide-label"];
36
52
  /** ElementInternals for form association (public for tests / advanced hosts). */
37
53
  readonly internals: ElementInternals;
38
54
  private defaultFormValue;
@@ -44,6 +60,14 @@ export declare abstract class FormAssociatedElement extends BaseElement {
44
60
  set invalid(value: boolean);
45
61
  get errorMessage(): string;
46
62
  set errorMessage(value: string);
63
+ get required(): boolean;
64
+ set required(value: boolean);
65
+ /** Help text rendered under the label (empty string hides it). */
66
+ get description(): string;
67
+ set description(value: string);
68
+ /** Keep the label for assistive tech but hide it visually. */
69
+ get hideLabel(): boolean;
70
+ set hideLabel(value: boolean);
47
71
  /** Value reported to the owning form (`null` omits the control). */
48
72
  protected abstract getFormValue(): FormValue;
49
73
  /** Restore a value from form reset / autocomplete. */
@@ -60,4 +84,12 @@ export declare abstract class FormAssociatedElement extends BaseElement {
60
84
  protected applyInvalidState(control: HTMLElement | null | undefined, messageEl: HTMLElement | null | undefined): void;
61
85
  /** Apply invalid ARIA to every focusable control in a multi-option field. */
62
86
  protected applyInvalidStateToControls(controls: Iterable<HTMLElement | null | undefined>, messageEl: HTMLElement | null | undefined): void;
87
+ /**
88
+ * Apply the shared field features to a rendered field: the `required`
89
+ * indicator + `aria-required` on the control, the `description` help text +
90
+ * `aria-describedby`, and the `hide-label` visually-hidden treatment (handled
91
+ * in CSS via `:host([hide-label])`). Call after setting the label's text
92
+ * content each update, since setting `textContent` clears the appended mark.
93
+ */
94
+ protected applyFieldSupport(labelEl: HTMLElement | null | undefined, control: HTMLElement | null | undefined, descriptionEl: HTMLElement | null | undefined): void;
63
95
  }
@@ -124,10 +124,61 @@ export const boeFormFieldErrorStyles = `
124
124
  `;
125
125
  export const FORM_ERROR_MESSAGE_ID = "boe-field-error";
126
126
  export const formErrorMessageMarkup = () => `<p part="error-message" id="${FORM_ERROR_MESSAGE_ID}" role="alert" hidden></p>`;
127
+ export const FORM_DESCRIPTION_ID = "boe-field-description";
128
+ /** Optional help text rendered under the label (shown only when `description` is set). */
129
+ export const formDescriptionMarkup = () => `<p part="description" id="${FORM_DESCRIPTION_ID}" hidden></p>`;
130
+ /**
131
+ * Shared chrome for the field-feature set: help-text `description`, the
132
+ * `required` indicator, and the visually-hidden `hide-label` treatment.
133
+ * Include alongside `boeFormFieldErrorStyles` in a field's stylesheet.
134
+ */
135
+ export const boeFormFieldSupportStyles = `
136
+ [part="description"] {
137
+ margin: 0;
138
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
139
+ font-size: 0.82rem;
140
+ line-height: 1.35;
141
+ }
142
+
143
+ [part="description"][hidden] {
144
+ display: none;
145
+ }
146
+
147
+ [part="label"] .boe-required-mark {
148
+ margin-inline-start: 2px;
149
+ color: var(--boe-token-surface-status-surface-error, #ed3757);
150
+ }
151
+
152
+ :host([hide-label]) [part="label"] {
153
+ position: absolute;
154
+ width: 1px;
155
+ height: 1px;
156
+ padding: 0;
157
+ margin: -1px;
158
+ overflow: hidden;
159
+ clip: rect(0, 0, 0, 0);
160
+ white-space: nowrap;
161
+ border: 0;
162
+ }
163
+ `;
127
164
  export class FormAssociatedElement extends BaseElement {
128
165
  static formAssociated = true;
129
166
  /** Attributes every form control should observe (merge into subclass lists). */
130
167
  static formObservedAttributes = ["name", "invalid", "error-message"];
168
+ /**
169
+ * `formObservedAttributes` plus the shared field-feature attributes
170
+ * (`required`, `description`, `hide-label`). A field that renders a label +
171
+ * `formDescriptionMarkup()` and calls `applyFieldSupport()` should observe
172
+ * these instead.
173
+ */
174
+ static fieldObservedAttributes = [
175
+ "name",
176
+ "invalid",
177
+ "error-message",
178
+ "required",
179
+ "description",
180
+ "hide-label",
181
+ ];
131
182
  /** ElementInternals for form association (public for tests / advanced hosts). */
132
183
  internals;
133
184
  defaultFormValue = null;
@@ -164,6 +215,31 @@ export class FormAssociatedElement extends BaseElement {
164
215
  this.removeAttribute("error-message");
165
216
  }
166
217
  }
218
+ get required() {
219
+ return this.hasAttribute("required");
220
+ }
221
+ set required(value) {
222
+ this.toggleAttribute("required", Boolean(value));
223
+ }
224
+ /** Help text rendered under the label (empty string hides it). */
225
+ get description() {
226
+ return this.getAttribute("description") ?? "";
227
+ }
228
+ set description(value) {
229
+ if (value) {
230
+ this.setAttribute("description", value);
231
+ }
232
+ else {
233
+ this.removeAttribute("description");
234
+ }
235
+ }
236
+ /** Keep the label for assistive tech but hide it visually. */
237
+ get hideLabel() {
238
+ return this.hasAttribute("hide-label");
239
+ }
240
+ set hideLabel(value) {
241
+ this.toggleAttribute("hide-label", Boolean(value));
242
+ }
167
243
  connectedCallback() {
168
244
  super.connectedCallback();
169
245
  if (!this.defaultCaptured) {
@@ -215,4 +291,42 @@ export class FormAssociatedElement extends BaseElement {
215
291
  }
216
292
  this.syncFormAssociation();
217
293
  }
294
+ /**
295
+ * Apply the shared field features to a rendered field: the `required`
296
+ * indicator + `aria-required` on the control, the `description` help text +
297
+ * `aria-describedby`, and the `hide-label` visually-hidden treatment (handled
298
+ * in CSS via `:host([hide-label])`). Call after setting the label's text
299
+ * content each update, since setting `textContent` clears the appended mark.
300
+ */
301
+ applyFieldSupport(labelEl, control, descriptionEl) {
302
+ const required = this.required;
303
+ const description = this.description;
304
+ if (labelEl) {
305
+ const existing = labelEl.querySelector(".boe-required-mark");
306
+ if (required && !existing) {
307
+ const mark = document.createElement("span");
308
+ mark.className = "boe-required-mark";
309
+ mark.setAttribute("aria-hidden", "true");
310
+ mark.textContent = "*";
311
+ labelEl.append(mark);
312
+ }
313
+ else if (!required && existing) {
314
+ existing.remove();
315
+ }
316
+ }
317
+ if (control) {
318
+ control.toggleAttribute("required", required);
319
+ control.setAttribute("aria-required", String(required));
320
+ if (description) {
321
+ control.setAttribute("aria-describedby", FORM_DESCRIPTION_ID);
322
+ }
323
+ else {
324
+ control.removeAttribute("aria-describedby");
325
+ }
326
+ }
327
+ if (descriptionEl) {
328
+ descriptionEl.textContent = description;
329
+ descriptionEl.hidden = description.length === 0;
330
+ }
331
+ }
218
332
  }
@@ -2,5 +2,5 @@ export { Controller } from "./controller.js";
2
2
  export { TypedEventEmitter } from "./event-emitter.js";
3
3
  export type { EventMap, Unsubscribe } from "./event-emitter.js";
4
4
  export { BaseElement } from "./element.js";
5
- export { FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, formDataFromNamedValues, formDataFromRange, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
5
+ export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
6
6
  export type { FormValue } from "./form-associated.js";
@@ -1,4 +1,4 @@
1
1
  export { Controller } from "./controller.js";
2
2
  export { TypedEventEmitter } from "./event-emitter.js";
3
3
  export { BaseElement } from "./element.js";
4
- export { FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, formDataFromNamedValues, formDataFromRange, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
4
+ export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
@@ -0,0 +1,2 @@
1
+ export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./positioning.js";
2
+ export type { OverlayAlign, OverlayPlacement, OverlaySide, PositionOptions, PositionResult, Rect, Size, } from "./positioning.js";
@@ -0,0 +1 @@
1
+ export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./positioning.js";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Viewport-aware anchored positioning for overlays (popover, tooltip, dropdown,
3
+ * menu, context-menu). box-open-elements has no portal, so floating surfaces
4
+ * position with `position: fixed` in viewport coordinates — which also lets them
5
+ * escape `overflow: hidden` / stacking-context ancestors, the job a portal does
6
+ * in box-ui-elements.
7
+ *
8
+ * The geometry core (`resolvePosition`) is pure and DOM-free so it can be unit
9
+ * tested with plain rects; the DOM helpers (`anchorFloating`, `trackAnchor`)
10
+ * read live rects and apply styles.
11
+ *
12
+ * Behaviour mirrors box-ui-elements' flyout: a preferred placement, `flip` to
13
+ * the opposite side when it would overflow, and `shift` along the cross axis to
14
+ * stay within the viewport.
15
+ */
16
+ /** Primary side the floating element sits on, relative to the anchor. */
17
+ export type OverlaySide = "top" | "bottom" | "left" | "right";
18
+ /** Cross-axis alignment of the floating element against the anchor edge. */
19
+ export type OverlayAlign = "start" | "center" | "end";
20
+ export interface OverlayPlacement {
21
+ side: OverlaySide;
22
+ align: OverlayAlign;
23
+ }
24
+ export interface Rect {
25
+ top: number;
26
+ left: number;
27
+ width: number;
28
+ height: number;
29
+ }
30
+ export interface Size {
31
+ width: number;
32
+ height: number;
33
+ }
34
+ export interface PositionOptions {
35
+ /** Preferred placement. Default `{ side: "bottom", align: "start" }`. */
36
+ placement?: OverlayPlacement;
37
+ /** Gap between the anchor edge and the floating element, in px. Default 4. */
38
+ offset?: number;
39
+ /** Flip to the opposite side when the preferred side would overflow. Default true. */
40
+ flip?: boolean;
41
+ /** Shift along the cross axis to keep the element inside the viewport. Default true. */
42
+ shift?: boolean;
43
+ /** Minimum gap kept from each viewport edge, in px. Default 8. */
44
+ padding?: number;
45
+ }
46
+ export interface PositionResult {
47
+ /** Viewport x (use as `left` for a `position: fixed` element). */
48
+ x: number;
49
+ /** Viewport y (use as `top`). */
50
+ y: number;
51
+ /** The side actually used (may differ from the request after a flip). */
52
+ side: OverlaySide;
53
+ /** The cross-axis alignment used. */
54
+ align: OverlayAlign;
55
+ }
56
+ /**
57
+ * Compute the viewport position of a floating element against an anchor. Pure —
58
+ * pass plain rects (e.g. from `getBoundingClientRect()`) and the viewport size.
59
+ */
60
+ export declare const resolvePosition: (anchor: Rect, floating: Size, viewport: Size, options?: PositionOptions) => PositionResult;
61
+ /** Parse a placement string like `"bottom-start"` / `"top"` / `"right-center"`. */
62
+ export declare const parsePlacement: (value: string | null | undefined) => OverlayPlacement | undefined;
63
+ /**
64
+ * Position `floating` against `anchor` once, applying `position: fixed` and the
65
+ * resolved coordinates. Returns the resolved placement (e.g. for arrow styling).
66
+ */
67
+ export declare const anchorFloating: (anchor: HTMLElement, floating: HTMLElement, options?: PositionOptions) => PositionResult;
68
+ /**
69
+ * Keep `floating` positioned against `anchor` while it is open: reposition on
70
+ * scroll (capture, to catch scrolling ancestors) and resize. Returns a cleanup
71
+ * that removes the listeners. Call `anchorFloating` once up front is handled
72
+ * internally.
73
+ */
74
+ export declare const trackAnchor: (anchor: HTMLElement, floating: HTMLElement, options?: PositionOptions, onReposition?: (result: PositionResult) => void) => (() => void);
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Viewport-aware anchored positioning for overlays (popover, tooltip, dropdown,
3
+ * menu, context-menu). box-open-elements has no portal, so floating surfaces
4
+ * position with `position: fixed` in viewport coordinates — which also lets them
5
+ * escape `overflow: hidden` / stacking-context ancestors, the job a portal does
6
+ * in box-ui-elements.
7
+ *
8
+ * The geometry core (`resolvePosition`) is pure and DOM-free so it can be unit
9
+ * tested with plain rects; the DOM helpers (`anchorFloating`, `trackAnchor`)
10
+ * read live rects and apply styles.
11
+ *
12
+ * Behaviour mirrors box-ui-elements' flyout: a preferred placement, `flip` to
13
+ * the opposite side when it would overflow, and `shift` along the cross axis to
14
+ * stay within the viewport.
15
+ */
16
+ const OPPOSITE = {
17
+ top: "bottom",
18
+ bottom: "top",
19
+ left: "right",
20
+ right: "left",
21
+ };
22
+ const isVertical = (side) => side === "top" || side === "bottom";
23
+ /** Cross-axis start coordinate for an alignment. */
24
+ const alignStart = (anchorStart, anchorSize, floatingSize, align) => {
25
+ if (align === "center")
26
+ return anchorStart + anchorSize / 2 - floatingSize / 2;
27
+ if (align === "end")
28
+ return anchorStart + anchorSize - floatingSize;
29
+ return anchorStart;
30
+ };
31
+ /** Main-axis coordinate for a side (top/left of the floating element). */
32
+ const sideCoord = (anchor, floating, side, offset) => {
33
+ switch (side) {
34
+ case "bottom":
35
+ return anchor.top + anchor.height + offset;
36
+ case "top":
37
+ return anchor.top - floating.height - offset;
38
+ case "right":
39
+ return anchor.left + anchor.width + offset;
40
+ case "left":
41
+ return anchor.left - floating.width - offset;
42
+ }
43
+ };
44
+ /** Space available between the anchor and the viewport edge on a given side. */
45
+ const spaceOn = (anchor, viewport, side) => {
46
+ switch (side) {
47
+ case "top":
48
+ return anchor.top;
49
+ case "bottom":
50
+ return viewport.height - (anchor.top + anchor.height);
51
+ case "left":
52
+ return anchor.left;
53
+ case "right":
54
+ return viewport.width - (anchor.left + anchor.width);
55
+ }
56
+ };
57
+ const clamp = (value, min, max) => max < min ? min : Math.min(Math.max(value, min), max);
58
+ /**
59
+ * Compute the viewport position of a floating element against an anchor. Pure —
60
+ * pass plain rects (e.g. from `getBoundingClientRect()`) and the viewport size.
61
+ */
62
+ export const resolvePosition = (anchor, floating, viewport, options = {}) => {
63
+ const { side: wantSide, align } = options.placement ?? { side: "bottom", align: "start" };
64
+ const offset = options.offset ?? 4;
65
+ const flip = options.flip ?? true;
66
+ const shift = options.shift ?? true;
67
+ const padding = options.padding ?? 8;
68
+ // Flip: if the preferred side lacks room for the element (+offset) and the
69
+ // opposite side has more, use the opposite side.
70
+ let side = wantSide;
71
+ if (flip) {
72
+ const need = (isVertical(wantSide) ? floating.height : floating.width) + offset;
73
+ const room = spaceOn(anchor, viewport, wantSide);
74
+ if (room < need) {
75
+ const opposite = OPPOSITE[wantSide];
76
+ if (spaceOn(anchor, viewport, opposite) > room)
77
+ side = opposite;
78
+ }
79
+ }
80
+ let x;
81
+ let y;
82
+ if (isVertical(side)) {
83
+ y = sideCoord(anchor, floating, side, offset);
84
+ x = alignStart(anchor.left, anchor.width, floating.width, align);
85
+ if (shift)
86
+ x = clamp(x, padding, viewport.width - floating.width - padding);
87
+ }
88
+ else {
89
+ x = sideCoord(anchor, floating, side, offset);
90
+ y = alignStart(anchor.top, anchor.height, floating.height, align);
91
+ if (shift)
92
+ y = clamp(y, padding, viewport.height - floating.height - padding);
93
+ }
94
+ return { x, y, side, align };
95
+ };
96
+ /** Parse a placement string like `"bottom-start"` / `"top"` / `"right-center"`. */
97
+ export const parsePlacement = (value) => {
98
+ if (!value)
99
+ return undefined;
100
+ const [rawSide, rawAlign] = value.trim().toLowerCase().split("-");
101
+ const sideMap = {
102
+ top: "top",
103
+ bottom: "bottom",
104
+ left: "left",
105
+ right: "right",
106
+ // Legacy popover names (start = inline-start = left, end = right).
107
+ start: "left",
108
+ end: "right",
109
+ };
110
+ const side = sideMap[rawSide];
111
+ if (!side)
112
+ return undefined;
113
+ const alignMap = {
114
+ start: "start",
115
+ center: "center",
116
+ middle: "center",
117
+ end: "end",
118
+ };
119
+ return { side, align: alignMap[rawAlign] ?? "start" };
120
+ };
121
+ const viewportSize = () => ({
122
+ width: window.innerWidth,
123
+ height: window.innerHeight,
124
+ });
125
+ /**
126
+ * Position `floating` against `anchor` once, applying `position: fixed` and the
127
+ * resolved coordinates. Returns the resolved placement (e.g. for arrow styling).
128
+ */
129
+ export const anchorFloating = (anchor, floating, options = {}) => {
130
+ const a = anchor.getBoundingClientRect();
131
+ const f = floating.getBoundingClientRect();
132
+ const result = resolvePosition({ top: a.top, left: a.left, width: a.width, height: a.height }, { width: f.width, height: f.height }, viewportSize(), options);
133
+ floating.style.position = "fixed";
134
+ floating.style.left = `${Math.round(result.x)}px`;
135
+ floating.style.top = `${Math.round(result.y)}px`;
136
+ floating.style.margin = "0";
137
+ return result;
138
+ };
139
+ /**
140
+ * Keep `floating` positioned against `anchor` while it is open: reposition on
141
+ * scroll (capture, to catch scrolling ancestors) and resize. Returns a cleanup
142
+ * that removes the listeners. Call `anchorFloating` once up front is handled
143
+ * internally.
144
+ */
145
+ export const trackAnchor = (anchor, floating, options = {}, onReposition) => {
146
+ const reposition = () => {
147
+ if (!anchor.isConnected || !floating.isConnected)
148
+ return;
149
+ // Compute+apply the position first, then notify. Do NOT inline this into
150
+ // `onReposition?.(anchorFloating(...))`: optional-call short-circuits its
151
+ // argument when the callback is undefined, so anchorFloating would never run.
152
+ const result = anchorFloating(anchor, floating, options);
153
+ onReposition?.(result);
154
+ };
155
+ reposition();
156
+ window.addEventListener("scroll", reposition, true);
157
+ window.addEventListener("resize", reposition);
158
+ return () => {
159
+ window.removeEventListener("scroll", reposition, true);
160
+ window.removeEventListener("resize", reposition);
161
+ };
162
+ };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export type { JsonSchema } from "./core/json-schema.js";
4
4
  export { DESIGN_SYSTEM_CHANGE_EVENT, applyDesignTokens, boxDarkDesignSystem, boxDefaultDesignSystem, createDesignTokenStyleText, getActiveDesignSystem, getDesignSystem, listDesignSystems, registerBoxDarkDesignSystem, registerBoxDefaultDesignSystem, registerDesignSystem, resolveDesignIcon, resolveDesignIllustration, resolveDesignSystemTokens, setActiveDesignSystem, } from "./foundations/tokens/index.js";
5
5
  export type { ApplyDesignTokensOptions, DesignAssetRenderContext, DesignAssetRenderer, DesignSystemDefinition, DesignTokenMap, } from "./foundations/tokens/index.js";
6
6
  export { boxIconography, boxIconographyAliases } from "./foundations/icons/index.js";
7
+ export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
8
+ export type { OverlayAlign, OverlayPlacement, OverlaySide, PositionOptions, PositionResult, } from "./foundations/overlay/index.js";
7
9
  export * from "./components/actions/button.js";
8
10
  export * from "./components/actions/button-group.js";
9
11
  export * from "./components/actions/icon-button.js";
@@ -17,10 +19,13 @@ export * from "./components/collections/datalist-item.js";
17
19
  export * from "./components/collections/draggable-list.js";
18
20
  export * from "./components/collections/grid-view.js";
19
21
  export * from "./components/collections/pagination.js";
22
+ export * from "./components/collections/table.js";
23
+ export * from "./components/collections/thumbnail-card.js";
20
24
  export * from "./components/collections/tree.js";
21
25
  export * from "./components/collections/tree-grid.js";
22
26
  export * from "./components/feedback/alert.js";
23
27
  export * from "./components/feedback/badge.js";
28
+ export * from "./components/feedback/badgeable.js";
24
29
  export * from "./components/feedback/chip.js";
25
30
  export * from "./components/feedback/empty-state.js";
26
31
  export * from "./components/feedback/error-mask.js";
@@ -36,6 +41,7 @@ export * from "./components/files/drop-zone.js";
36
41
  export * from "./components/identity/avatar.js";
37
42
  export * from "./components/identity/persona.js";
38
43
  export * from "./components/navigation/accordion.js";
44
+ export * from "./components/navigation/breadcrumb.js";
39
45
  export * from "./components/navigation/tabs.js";
40
46
  export * from "./components/visuals/illustration.js";
41
47
  export * from "./components/forms/checkbox.js";
@@ -71,8 +77,10 @@ export * from "./components/layout/nav-sidebar.js";
71
77
  export * from "./components/layout/section.js";
72
78
  export * from "./components/layout/sidebar-toggle-button.js";
73
79
  export * from "./components/layout/split-view.js";
80
+ export * from "./components/overlays/context-menu.js";
74
81
  export * from "./components/overlays/dialog.js";
75
82
  export * from "./components/overlays/drawer.js";
83
+ export * from "./components/overlays/guide-tooltip.js";
76
84
  export * from "./components/overlays/popover.js";
77
85
  export * from "./components/overlays/tooltip.js";
78
86
  export * from "./patterns/content-explorer/index.js";
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export { Controller, TypedEventEmitter } from "./core/index.js";
3
3
  // Foundations
4
4
  export { DESIGN_SYSTEM_CHANGE_EVENT, applyDesignTokens, boxDarkDesignSystem, boxDefaultDesignSystem, createDesignTokenStyleText, getActiveDesignSystem, getDesignSystem, listDesignSystems, registerBoxDarkDesignSystem, registerBoxDefaultDesignSystem, registerDesignSystem, resolveDesignIcon, resolveDesignIllustration, resolveDesignSystemTokens, setActiveDesignSystem, } from "./foundations/tokens/index.js";
5
5
  export { boxIconography, boxIconographyAliases } from "./foundations/icons/index.js";
6
+ // Foundations: viewport-aware overlay positioning (for building custom overlays)
7
+ export { anchorFloating, parsePlacement, resolvePosition, trackAnchor, } from "./foundations/overlay/index.js";
6
8
  // Components: actions
7
9
  export * from "./components/actions/button.js";
8
10
  export * from "./components/actions/button-group.js";
@@ -18,11 +20,14 @@ export * from "./components/collections/datalist-item.js";
18
20
  export * from "./components/collections/draggable-list.js";
19
21
  export * from "./components/collections/grid-view.js";
20
22
  export * from "./components/collections/pagination.js";
23
+ export * from "./components/collections/table.js";
24
+ export * from "./components/collections/thumbnail-card.js";
21
25
  export * from "./components/collections/tree.js";
22
26
  export * from "./components/collections/tree-grid.js";
23
27
  // Components: feedback
24
28
  export * from "./components/feedback/alert.js";
25
29
  export * from "./components/feedback/badge.js";
30
+ export * from "./components/feedback/badgeable.js";
26
31
  export * from "./components/feedback/chip.js";
27
32
  export * from "./components/feedback/empty-state.js";
28
33
  export * from "./components/feedback/error-mask.js";
@@ -41,6 +46,7 @@ export * from "./components/identity/avatar.js";
41
46
  export * from "./components/identity/persona.js";
42
47
  // Components: navigation
43
48
  export * from "./components/navigation/accordion.js";
49
+ export * from "./components/navigation/breadcrumb.js";
44
50
  export * from "./components/navigation/tabs.js";
45
51
  // Components: visuals
46
52
  export * from "./components/visuals/illustration.js";
@@ -80,8 +86,10 @@ export * from "./components/layout/section.js";
80
86
  export * from "./components/layout/sidebar-toggle-button.js";
81
87
  export * from "./components/layout/split-view.js";
82
88
  // Components: overlays
89
+ export * from "./components/overlays/context-menu.js";
83
90
  export * from "./components/overlays/dialog.js";
84
91
  export * from "./components/overlays/drawer.js";
92
+ export * from "./components/overlays/guide-tooltip.js";
85
93
  export * from "./components/overlays/popover.js";
86
94
  export * from "./components/overlays/tooltip.js";
87
95
  // Patterns: content explorer (headless blocks, contracts, transport, adapters)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unofficialbox/box-open-elements",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
5
5
  "license": "MIT",
6
6
  "homepage": "https://unofficialbox.github.io/box-open-elements",