tyrell-components 1.0.0-TC46 → 1.0.0-TC47

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.
@@ -6,4 +6,4 @@
6
6
  * circles read the matching accent var directly (no `-bg` indirection), and
7
7
  * the brand-layer flavor seeds cascade through for per-shade control.
8
8
  */
9
- export declare const wizardStyles = "\n/* Theming tokens \u2014 every default chains back to the brand layer / global\n scale tokens. */\n\n:host {\n display: block;\n width: var(--ty-wizard-width, 100%); /* set by width attribute \u2014 not a public token */\n height: var(--ty-wizard-height, 700px); /* set by height attribute \u2014 not a public token */\n box-sizing: border-box;\n\n /* State accents \u2014 the primary override surface. One variable retints the\n * matching circle background, border, and glow. */\n --ty-wizard-active-accent: var(--ty-color-primary);\n --ty-wizard-completed-accent: var(--ty-color-success);\n --ty-wizard-error-accent: var(--ty-color-danger);\n --ty-wizard-pending-accent: var(--ty-color-neutral);\n\n /* Container chrome \u2014 routes through global scales. Override --ty-radius-lg\n * or --ty-shadow-md app-wide and the wizard follows; override these locally\n * to give the wizard a distinct look. */\n --ty-wizard-bg: var(--ty-surface-floating);\n --ty-wizard-border: var(--ty-border);\n --ty-wizard-radius: var(--ty-radius-lg, 12px);\n --ty-wizard-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n\n --ty-wizard-header-bg: var(--ty-surface-content);\n --ty-wizard-header-border: var(--ty-border-soft, var(--ty-border));\n --ty-wizard-header-padding: 24px 24px 16px;\n\n --ty-wizard-progress-track: var(--ty-border);\n --ty-wizard-progress-fill: var(--ty-wizard-completed-accent);\n --ty-wizard-progress-height: 2px;\n\n /* Motion \u2014 routes through global motion tokens if defined. */\n --ty-wizard-transition-duration: var(--ty-transition-duration, 300ms);\n --ty-wizard-transition-easing: var(--ty-transition-easing, ease-in-out);\n\n --ty-wizard-circle-size: 32px;\n --ty-wizard-circle-border-width: 2px;\n\n --ty-wizard-panels-bg: var(--ty-surface-elevated);\n\n --ty-wizard-focus-outline: var(--ty-wizard-active-accent);\n}\n\n.wizard-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n border-radius: var(--ty-wizard-radius);\n border: 1px solid var(--ty-wizard-border);\n background: var(--ty-wizard-bg);\n box-shadow: var(--ty-wizard-shadow);\n overflow: hidden;\n}\n\n.step-indicators-wrapper {\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n position: relative;\n padding: var(--ty-wizard-header-padding);\n border-bottom: 1px solid var(--ty-wizard-header-border);\n background: var(--ty-wizard-header-bg);\n}\n\n/* Progress line, behind the step circles */\n.progress-line {\n position: absolute;\n /*\n * With equal-width indicators (flex: 1), each takes 100%/N of the width.\n * Circle centers sit at 50%/N from the left edge of each indicator.\n * Inset = 50% / step-count from each side keeps the line between circle centres.\n */\n left: calc(50% / var(--ty-wizard-step-count, 4));\n right: calc(50% / var(--ty-wizard-step-count, 4));\n top: calc(var(--ty-wizard-circle-size, 32px) / 2 - 1px);\n height: var(--ty-wizard-progress-height);\n background: var(--ty-wizard-progress-track);\n z-index: 0;\n pointer-events: none;\n transition: opacity var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n}\n\n.progress-overlay {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--ty-wizard-progress-fill);\n transition: width var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: width;\n}\n\n.step-indicators {\n display: flex;\n align-items: flex-start;\n position: relative;\n}\n\n.step-indicator {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n background: transparent;\n border: none;\n padding: 0;\n font: inherit;\n transition: opacity 200ms;\n flex: 1;\n min-width: 0;\n}\n\n.step-indicator[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.step-indicator:focus-visible {\n outline: 2px solid var(--ty-wizard-focus-outline);\n outline-offset: 4px;\n border-radius: 50%;\n}\n\n.step-circle {\n width: var(--ty-wizard-circle-size);\n height: var(--ty-wizard-circle-size);\n border-radius: 50%;\n border: var(--ty-wizard-circle-border-width) solid;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: var(--ty-local-transition, all var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing));\n box-sizing: border-box;\n flex-shrink: 0;\n}\n\n.step-circle[data-state=\"completed\"] {\n background: var(--ty-wizard-completed-accent);\n border-color: var(--ty-color-success-strong);\n /* Derived from the accent this circle actually paints, not from\n --ty-solid-success-fg: the two diverge in dark mode (--ty-solid-l\n dims solid fills by 0.25 L), which stranded white text at 3.5:1. */\n color: oklch(\n from var(--ty-wizard-completed-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.6) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-completed-accent) 12%, transparent);\n}\n\n.step-circle[data-state=\"active\"] {\n background: var(--ty-wizard-active-accent);\n border-color: var(--ty-color-primary-strong);\n color: oklch(\n from var(--ty-wizard-active-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.6) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-active-accent) 12%, transparent);\n}\n\n.step-circle[data-state=\"pending\"] {\n background: var(--ty-surface-elevated);\n border-color: var(--ty-border);\n color: var(--ty-color-neutral);\n}\n\n.step-circle[data-state=\"error\"] {\n background: var(--ty-wizard-error-accent);\n border-color: var(--ty-color-danger-strong);\n color: oklch(\n from var(--ty-wizard-error-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.6) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-error-accent) 12%, transparent);\n}\n\n.step-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n}\n\n.step-label {\n font-size: var(--ty-font-sm, 14px);\n font-weight: var(--ty-font-semibold, 600);\n color: var(--ty-color-neutral);\n transition: var(--ty-local-transition, color 200ms);\n}\n\n.step-indicator[aria-selected=\"true\"] .step-label {\n color: var(--ty-color-neutral-strong);\n}\n\n.step-indicator[aria-selected=\"false\"] .step-label {\n color: var(--ty-color-neutral);\n}\n\n.step-description {\n font-size: var(--ty-font-xs, 12px);\n font-weight: var(--ty-font-normal, 400);\n color: var(--ty-color-neutral);\n transition: var(--ty-local-transition, color 200ms);\n text-align: center;\n max-width: 120px;\n}\n\n.step-indicator[aria-selected=\"true\"] .step-description {\n color: var(--ty-color-neutral-bold);\n}\n\n/* Custom indicator content via slots */\n::slotted([slot^=\"indicator-\"]) {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n min-height: 0;\n background: var(--ty-wizard-panels-bg);\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: transform;\n}\n\n::slotted(ty-step) {\n width: var(--ty-wizard-width, 100%);\n height: 100%;\n flex-shrink: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .progress-overlay {\n transition-duration: 0ms !important;\n }\n\n .step-circle {\n transition-duration: 0ms !important;\n }\n\n .progress-line {\n transition-duration: 0ms !important;\n }\n}\n\n.sr-only {\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-width: 0;\n}\n";
9
+ export declare const wizardStyles = "\n/* Theming tokens \u2014 every default chains back to the brand layer / global\n scale tokens. */\n\n:host {\n display: block;\n width: var(--ty-wizard-width, 100%); /* set by width attribute \u2014 not a public token */\n height: var(--ty-wizard-height, 700px); /* set by height attribute \u2014 not a public token */\n box-sizing: border-box;\n\n /* State accents \u2014 the primary override surface. One variable retints the\n * matching circle background, border, and glow. */\n --ty-wizard-active-accent: var(--ty-color-primary);\n --ty-wizard-completed-accent: var(--ty-color-success);\n --ty-wizard-error-accent: var(--ty-color-danger);\n --ty-wizard-pending-accent: var(--ty-color-neutral);\n\n /* Container chrome \u2014 routes through global scales. Override --ty-radius-lg\n * or --ty-shadow-md app-wide and the wizard follows; override these locally\n * to give the wizard a distinct look. */\n --ty-wizard-bg: var(--ty-surface-floating);\n --ty-wizard-border: var(--ty-border);\n --ty-wizard-radius: var(--ty-radius-lg, 12px);\n --ty-wizard-shadow: var(--ty-shadow-md, 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1));\n\n --ty-wizard-header-bg: var(--ty-surface-content);\n --ty-wizard-header-border: var(--ty-border-soft, var(--ty-border));\n --ty-wizard-header-padding: 24px 24px 16px;\n\n --ty-wizard-progress-track: var(--ty-border);\n --ty-wizard-progress-fill: var(--ty-wizard-completed-accent);\n --ty-wizard-progress-height: 2px;\n\n /* Motion \u2014 routes through global motion tokens if defined. */\n --ty-wizard-transition-duration: var(--ty-transition-duration, 300ms);\n --ty-wizard-transition-easing: var(--ty-transition-easing, ease-in-out);\n\n --ty-wizard-circle-size: 32px;\n --ty-wizard-circle-border-width: 2px;\n\n --ty-wizard-panels-bg: var(--ty-surface-elevated);\n\n --ty-wizard-focus-outline: var(--ty-wizard-active-accent);\n}\n\n.wizard-container {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n box-sizing: border-box;\n border-radius: var(--ty-wizard-radius);\n border: 1px solid var(--ty-wizard-border);\n background: var(--ty-wizard-bg);\n box-shadow: var(--ty-wizard-shadow);\n overflow: hidden;\n}\n\n.step-indicators-wrapper {\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n position: relative;\n padding: var(--ty-wizard-header-padding);\n border-bottom: 1px solid var(--ty-wizard-header-border);\n background: var(--ty-wizard-header-bg);\n}\n\n/* Progress line, behind the step circles */\n.progress-line {\n position: absolute;\n /*\n * With equal-width indicators (flex: 1), each takes 100%/N of the width.\n * Circle centers sit at 50%/N from the left edge of each indicator.\n * Inset = 50% / step-count from each side keeps the line between circle centres.\n */\n left: calc(50% / var(--ty-wizard-step-count, 4));\n right: calc(50% / var(--ty-wizard-step-count, 4));\n top: calc(var(--ty-wizard-circle-size, 32px) / 2 - 1px);\n height: var(--ty-wizard-progress-height);\n background: var(--ty-wizard-progress-track);\n z-index: 0;\n pointer-events: none;\n transition: opacity var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n}\n\n.progress-overlay {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--ty-wizard-progress-fill);\n transition: width var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: width;\n}\n\n.step-indicators {\n display: flex;\n align-items: flex-start;\n position: relative;\n}\n\n.step-indicator {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n background: transparent;\n border: none;\n padding: 0;\n font: inherit;\n transition: opacity 200ms;\n flex: 1;\n min-width: 0;\n}\n\n.step-indicator[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.step-indicator:focus-visible {\n outline: 2px solid var(--ty-wizard-focus-outline);\n outline-offset: 4px;\n border-radius: 50%;\n}\n\n.step-circle {\n width: var(--ty-wizard-circle-size);\n height: var(--ty-wizard-circle-size);\n border-radius: 50%;\n border: var(--ty-wizard-circle-border-width) solid;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n z-index: 10;\n transition: var(--ty-local-transition, all var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing));\n box-sizing: border-box;\n flex-shrink: 0;\n}\n\n.step-circle[data-state=\"completed\"] {\n background: var(--ty-wizard-completed-accent);\n border-color: var(--ty-color-success-strong);\n /* Derived from the accent this circle actually paints, not from\n --ty-solid-success-fg: the two diverge in dark mode (--ty-solid-l\n dims solid fills by 0.25 L), which stranded white text at 3.5:1. */\n color: oklch(\n from var(--ty-wizard-completed-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-completed-accent) 12%, transparent);\n}\n\n.step-circle[data-state=\"active\"] {\n background: var(--ty-wizard-active-accent);\n border-color: var(--ty-color-primary-strong);\n color: oklch(\n from var(--ty-wizard-active-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-active-accent) 12%, transparent);\n}\n\n.step-circle[data-state=\"pending\"] {\n background: var(--ty-surface-elevated);\n border-color: var(--ty-border);\n color: var(--ty-color-neutral);\n}\n\n.step-circle[data-state=\"error\"] {\n background: var(--ty-wizard-error-accent);\n border-color: var(--ty-color-danger-strong);\n color: oklch(\n from var(--ty-wizard-error-accent)\n clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0\n );\n box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-error-accent) 12%, transparent);\n}\n\n.step-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n}\n\n.step-label {\n font-size: var(--ty-font-sm, 14px);\n font-weight: var(--ty-font-semibold, 600);\n color: var(--ty-color-neutral);\n transition: var(--ty-local-transition, color 200ms);\n}\n\n.step-indicator[aria-selected=\"true\"] .step-label {\n color: var(--ty-color-neutral-strong);\n}\n\n.step-indicator[aria-selected=\"false\"] .step-label {\n color: var(--ty-color-neutral);\n}\n\n.step-description {\n font-size: var(--ty-font-xs, 12px);\n font-weight: var(--ty-font-normal, 400);\n color: var(--ty-color-neutral);\n transition: var(--ty-local-transition, color 200ms);\n text-align: center;\n max-width: 120px;\n}\n\n.step-indicator[aria-selected=\"true\"] .step-description {\n color: var(--ty-color-neutral-bold);\n}\n\n/* Custom indicator content via slots */\n::slotted([slot^=\"indicator-\"]) {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n\n.panels-viewport {\n position: relative;\n flex: 1;\n overflow: hidden;\n min-height: 0;\n background: var(--ty-wizard-panels-bg);\n}\n\n/* Panels wrapper slides horizontally */\n.panels-wrapper {\n display: flex;\n height: 100%;\n transition: transform var(--ty-wizard-transition-duration) var(--ty-wizard-transition-easing);\n will-change: transform;\n}\n\n::slotted(ty-step) {\n width: var(--ty-wizard-width, 100%);\n height: 100%;\n flex-shrink: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panels-wrapper {\n transition-duration: 0ms !important;\n }\n\n .progress-overlay {\n transition-duration: 0ms !important;\n }\n\n .step-circle {\n transition-duration: 0ms !important;\n }\n\n .progress-line {\n transition-duration: 0ms !important;\n }\n}\n\n.sr-only {\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-width: 0;\n}\n";
@@ -151,7 +151,7 @@ export const wizardStyles = `
151
151
  dims solid fills by 0.25 L), which stranded white text at 3.5:1. */
152
152
  color: oklch(
153
153
  from var(--ty-wizard-completed-accent)
154
- clamp(0, (var(--ty-solid-fg-threshold, 0.6) - l) * 1000, 1) 0 0
154
+ clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0
155
155
  );
156
156
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-completed-accent) 12%, transparent);
157
157
  }
@@ -161,7 +161,7 @@ export const wizardStyles = `
161
161
  border-color: var(--ty-color-primary-strong);
162
162
  color: oklch(
163
163
  from var(--ty-wizard-active-accent)
164
- clamp(0, (var(--ty-solid-fg-threshold, 0.6) - l) * 1000, 1) 0 0
164
+ clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0
165
165
  );
166
166
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-active-accent) 12%, transparent);
167
167
  }
@@ -177,7 +177,7 @@ export const wizardStyles = `
177
177
  border-color: var(--ty-color-danger-strong);
178
178
  color: oklch(
179
179
  from var(--ty-wizard-error-accent)
180
- clamp(0, (var(--ty-solid-fg-threshold, 0.6) - l) * 1000, 1) 0 0
180
+ clamp(0, (var(--ty-solid-fg-threshold, 0.57) - l) * 1000, 1) 0 0
181
181
  );
182
182
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--ty-wizard-error-accent) 12%, transparent);
183
183
  }
@@ -12,7 +12,7 @@ export type PlacementOrientation = 'horizontal' | 'vertical';
12
12
  /**
13
13
  * Vertical alignment options
14
14
  */
15
- export type VerticalAlign = 'top' | 'center' | 'bottom' | 'end';
15
+ export type VerticalAlign = 'top' | 'start' | 'center' | 'bottom' | 'end';
16
16
  /**
17
17
  * Horizontal alignment options
18
18
  */
@@ -110,6 +110,47 @@ export declare const placementPreferences: {
110
110
  tooltip: Placement[];
111
111
  dropdown: Placement[];
112
112
  };
113
+ /** The four sides a floating element can sit on. */
114
+ export type PlacementSide = 'top' | 'right' | 'bottom' | 'left';
115
+ /** Cross-axis alignment against the anchor. Omitted means centred. */
116
+ export type PlacementAlign = 'start' | 'end';
117
+ /** Split `'left-end'` into `{ side: 'left', align: 'end' }`. */
118
+ export declare function parsePlacement(placement: Placement): {
119
+ side: PlacementSide;
120
+ align?: PlacementAlign;
121
+ };
122
+ /**
123
+ * Fallback order to try for a requested placement, best-fit first.
124
+ *
125
+ * FLIP BEFORE RE-ALIGN. When a placement overflows it is almost always the
126
+ * SIDE axis that ran out of room, and re-aligning on the same side does not
127
+ * change the fit at all: `bottom-start` and `bottom-end` need identical
128
+ * vertical space. Flipping to the opposite side is the move that actually
129
+ * helps, so the requested alignment is carried across the flip first, and only
130
+ * then do we try other alignments.
131
+ *
132
+ * This also keeps the old hand-written chains intact for the four bare sides —
133
+ * `top` still degrades to `bottom` as its very next candidate, exactly as
134
+ * before aligned placements existed.
135
+ *
136
+ * Callers previously hand-wrote a 4-entry chain per side, which silently
137
+ * dropped every aligned placement onto the default chain.
138
+ */
139
+ export declare function preferenceChain(placement: Placement): Placement[];
140
+ /**
141
+ * Map a `Placement` onto `computeAnchoredPosition`'s inputs, so dropdown-style
142
+ * components (ty-select, ty-date-picker) accept the same `placement` vocabulary
143
+ * as ty-popup / ty-tooltip.
144
+ *
145
+ * Dropdowns only ever live above or below their trigger, so a left/right
146
+ * placement degrades to "auto" side while keeping its alignment. Bare sides
147
+ * default to start-aligned, matching a native <select> and the behaviour these
148
+ * components had before `placement` existed.
149
+ */
150
+ export declare function placementToAnchored(placement?: Placement | '' | null): {
151
+ side: 'top' | 'bottom' | 'auto';
152
+ align: 'start' | 'center' | 'end';
153
+ };
113
154
  /**
114
155
  * Find the best position for the floating element
115
156
  * Tries all preference placements and returns the one that fits best
@@ -131,10 +172,14 @@ export interface AnchoredPopupOptions {
131
172
  gap?: number;
132
173
  /** Minimum distance from viewport edges (px, default 8) */
133
174
  padding?: number;
134
- /** Horizontal anchor edge: trigger's left edge ("start", default) or
135
- * right edge ("end"). Either way the result is clamped into the
136
- * viewport, same as before. */
137
- align?: "start" | "end";
175
+ /** Horizontal anchor edge: trigger's left edge ("start", default), its
176
+ * right edge ("end"), or centred on it. Either way the result is clamped
177
+ * into the viewport, same as before. */
178
+ align?: "start" | "center" | "end";
179
+ /** Preferred vertical side. "auto" (default) keeps the historic behaviour:
180
+ * below when it fits, otherwise whichever side has more room. Naming a
181
+ * side honours it when it fits and still flips rather than clipping. */
182
+ side?: "top" | "bottom" | "auto";
138
183
  }
139
184
  export interface AnchoredPopupPosition {
140
185
  /** Popup left edge, viewport coords, clamped into view */
@@ -12,7 +12,7 @@ export const placements = {
12
12
  horizontal: 'end',
13
13
  },
14
14
  'right-start': {
15
- vertical: 'center',
15
+ vertical: 'start',
16
16
  horizontal: 'end',
17
17
  orientation: 'vertical',
18
18
  },
@@ -39,7 +39,7 @@ export const placements = {
39
39
  horizontal: 'end',
40
40
  },
41
41
  'left-start': {
42
- vertical: 'center',
42
+ vertical: 'start',
43
43
  horizontal: 'start',
44
44
  orientation: 'vertical',
45
45
  },
@@ -68,6 +68,41 @@ export const placementPreferences = {
68
68
  'bottom', 'top', 'right', 'left'
69
69
  ],
70
70
  };
71
+ const OPPOSITE_SIDE = {
72
+ top: 'bottom',
73
+ bottom: 'top',
74
+ left: 'right',
75
+ right: 'left',
76
+ };
77
+ export function parsePlacement(placement) {
78
+ const [side, align] = placement.split('-');
79
+ return { side, align };
80
+ }
81
+ export function preferenceChain(placement) {
82
+ const { side, align } = parsePlacement(placement);
83
+ const opposite = OPPOSITE_SIDE[side];
84
+ const perpendicular = side === 'top' || side === 'bottom' ? ['right', 'left'] : ['bottom', 'top'];
85
+ const exact = (s) => (align ? `${s}-${align}` : s);
86
+ const rest = (s) => (align
87
+ ? [s, `${s}-${align === 'start' ? 'end' : 'start'}`]
88
+ : [`${s}-start`, `${s}-end`]);
89
+ return [
90
+ exact(side),
91
+ exact(opposite),
92
+ ...rest(side),
93
+ ...rest(opposite),
94
+ ...perpendicular.flatMap((s) => [exact(s), ...rest(s)]),
95
+ ];
96
+ }
97
+ export function placementToAnchored(placement) {
98
+ if (!placement)
99
+ return { side: 'auto', align: 'start' };
100
+ const { side, align } = parsePlacement(placement);
101
+ return {
102
+ side: side === 'top' || side === 'bottom' ? side : 'auto',
103
+ align: align ?? 'start',
104
+ };
105
+ }
71
106
  function getElementRect(el) {
72
107
  const rect = el.getBoundingClientRect();
73
108
  return {
@@ -123,7 +158,7 @@ function calculatePlacement(options) {
123
158
  y = targetRect.bottom - floatingRect.height - containerPadding;
124
159
  }
125
160
  else {
126
- y = targetRect.top;
161
+ y = targetRect.top + containerPadding;
127
162
  }
128
163
  }
129
164
  else {
@@ -178,8 +213,17 @@ export function computeAnchoredPosition(o) {
178
213
  const vh = window.innerHeight;
179
214
  const spaceBelow = vh - o.anchorRect.bottom;
180
215
  const spaceAbove = o.anchorRect.top;
181
- const below = spaceBelow >= o.popupHeight + gap + padding || spaceBelow >= spaceAbove;
182
- const rawX = o.align === "end" ? o.anchorRect.right - o.popupWidth : o.anchorRect.left;
216
+ const needed = o.popupHeight + gap + padding;
217
+ const fitsBelow = spaceBelow >= needed;
218
+ const fitsAbove = spaceAbove >= needed;
219
+ const side = o.side ?? "auto";
220
+ const below = side === "bottom" ? (fitsBelow || !fitsAbove ? true : false)
221
+ : side === "top" ? (fitsAbove || !fitsBelow ? false : true)
222
+ : fitsBelow || spaceBelow >= spaceAbove;
223
+ const rawX = o.align === "end" ? o.anchorRect.right - o.popupWidth
224
+ : o.align === "center"
225
+ ? o.anchorRect.left + (o.anchorRect.width - o.popupWidth) / 2
226
+ : o.anchorRect.left;
183
227
  const x = Math.max(padding, Math.min(rawX, vw - o.popupWidth - padding));
184
228
  return {
185
229
  x,
package/lib/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * This is automatically synced with package.json during build.
5
5
  * Use `npm version` to bump versions.
6
6
  */
7
- export declare const VERSION = "1.0.0-TC46";
7
+ export declare const VERSION = "1.0.0-TC47";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.0-TC46';
1
+ export const VERSION = '1.0.0-TC47';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyrell-components",
3
- "version": "1.0.0-TC46",
3
+ "version": "1.0.0-TC47",
4
4
  "description": "Tyrell Components - Framework-agnostic web components with semantic design system",
5
5
  "type": "module",
6
6
  "sideEffects": [