@inf-monkeys-tech/monkeys-design 1.0.44 → 1.0.45

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/dist/index.mjs CHANGED
@@ -22617,13 +22617,146 @@ function WorkbenchMasonryLayout({
22617
22617
  }
22618
22618
  );
22619
22619
  }
22620
+ var DEFAULT_PROVISIONAL_CAPACITY = 12;
22621
+ var DEFAULT_GRID_ASPECT_RATIOS = [1];
22622
+ var DEFAULT_RAIL_ASPECT_RATIOS = [4 / 3];
22623
+ var DEFAULT_MASONRY_ASPECT_RATIOS = [1, 4 / 5, 3 / 2, 3 / 4, 5 / 4];
22624
+ var MIN_SKELETON_ASPECT_RATIO = 0.5;
22625
+ var MAX_SKELETON_ASPECT_RATIO = 2;
22626
+ var SKELETON_BACKGROUND_COLOR = "hsl(var(--monkeys-color-muted, var(--muted)))";
22627
+ function clamp4(value, min5, max5) {
22628
+ return Math.min(Math.max(value, min5), max5);
22629
+ }
22630
+ function normalizeCount(value, fallback, max5) {
22631
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallback;
22632
+ return clamp4(Math.round(value), 1, max5);
22633
+ }
22634
+ function getDefaultAspectRatios(mode) {
22635
+ if (mode === "masonry" || mode === "dense") return DEFAULT_MASONRY_ASPECT_RATIOS;
22636
+ if (mode === "rail") return DEFAULT_RAIL_ASPECT_RATIOS;
22637
+ return DEFAULT_GRID_ASPECT_RATIOS;
22638
+ }
22639
+ function resolveAspectRatios(mode, values) {
22640
+ const normalized = values?.filter((value) => typeof value === "number" && Number.isFinite(value) && value > 0).map((value) => clamp4(value, MIN_SKELETON_ASPECT_RATIO, MAX_SKELETON_ASPECT_RATIO));
22641
+ return normalized?.length ? normalized : getDefaultAspectRatios(mode);
22642
+ }
22643
+ function resolveWorkbenchCollectionSkeletonCapacity(config) {
22644
+ return normalizeCount(config?.count, DEFAULT_PROVISIONAL_CAPACITY, 48);
22645
+ }
22646
+ function resolveWorkbenchCollectionLoadingVariant(value) {
22647
+ if (value === void 0) return "skeleton";
22648
+ if (value === "skeleton" || value === "spinner") return value;
22649
+ throw new TypeError('WorkbenchCollection loading.variant must be "skeleton" or "spinner".');
22650
+ }
22651
+ function resolveSkeletonCount({
22652
+ columnCount,
22653
+ config,
22654
+ phase
22655
+ }) {
22656
+ if (phase === "initial") {
22657
+ const fallback2 = clamp4(columnCount * 2, 6, 24);
22658
+ return normalizeCount(config?.count, fallback2, 48);
22659
+ }
22660
+ const fallback = clamp4(columnCount, 1, 12);
22661
+ return normalizeCount(config?.loadMoreCount, fallback, 24);
22662
+ }
22663
+ function getSkeletonHeight(columnWidth, aspectRatio) {
22664
+ return Math.max(1, columnWidth / aspectRatio);
22665
+ }
22666
+ function WorkbenchCollectionSkeleton({
22667
+ mode,
22668
+ phase,
22669
+ columnCount,
22670
+ columnWidth,
22671
+ cardWidth,
22672
+ gap,
22673
+ rowHeight,
22674
+ config,
22675
+ ariaLabel,
22676
+ className
22677
+ }) {
22678
+ const count = resolveSkeletonCount({ columnCount, config, phase });
22679
+ const aspectRatios = resolveAspectRatios(mode, config?.aspectRatios);
22680
+ const animated = config?.animated ?? true;
22681
+ const items = useMemo(
22682
+ () => Array.from({ length: count }, (_, index) => ({
22683
+ aspectRatio: aspectRatios[index % aspectRatios.length],
22684
+ key: `${phase}-${index}`
22685
+ })),
22686
+ [aspectRatios, count, phase]
22687
+ );
22688
+ const renderSkeleton = (item, style) => /* @__PURE__ */ jsx(
22689
+ BaseSkeleton,
22690
+ {
22691
+ variant: "block",
22692
+ animated,
22693
+ className: "h-auto w-full",
22694
+ style: {
22695
+ backgroundColor: SKELETON_BACKGROUND_COLOR,
22696
+ ...style ?? { aspectRatio: String(item.aspectRatio) }
22697
+ },
22698
+ "data-collection-skeleton-item": "",
22699
+ "data-skeleton-aspect-ratio": item.aspectRatio
22700
+ }
22701
+ );
22702
+ return /* @__PURE__ */ jsx(
22703
+ "div",
22704
+ {
22705
+ role: "status",
22706
+ "aria-label": ariaLabel,
22707
+ "aria-atomic": "true",
22708
+ "data-collection-loading": phase,
22709
+ "data-layout-mode": mode,
22710
+ className: "min-w-0",
22711
+ children: mode === "masonry" || mode === "dense" ? /* @__PURE__ */ jsx(
22712
+ WorkbenchMasonryLayout,
22713
+ {
22714
+ items,
22715
+ columnCount,
22716
+ columnWidth,
22717
+ gap,
22718
+ rowHeight,
22719
+ placementStrategy: mode === "dense" ? "dense" : "shortest",
22720
+ className,
22721
+ containerProps: { "aria-hidden": true },
22722
+ getItemKey: (item) => item.key,
22723
+ getItemSize: (item, layout) => ({
22724
+ height: getSkeletonHeight(layout.columnWidth, item.aspectRatio)
22725
+ }),
22726
+ renderItem: (item, layout) => renderSkeleton(item, {
22727
+ height: `${getSkeletonHeight(layout.columnWidth, item.aspectRatio)}px`
22728
+ })
22729
+ }
22730
+ ) : mode === "rail" ? /* @__PURE__ */ jsx(
22731
+ "div",
22732
+ {
22733
+ "aria-hidden": "true",
22734
+ className: cn("grid min-w-0 grid-flow-col items-start overflow-x-auto", className),
22735
+ style: {
22736
+ gap,
22737
+ gridAutoColumns: typeof cardWidth === "number" ? `${cardWidth}px` : cardWidth ?? `${columnWidth}px`
22738
+ },
22739
+ children: items.map((item) => /* @__PURE__ */ jsx("div", { children: renderSkeleton(item) }, item.key))
22740
+ }
22741
+ ) : /* @__PURE__ */ jsx(
22742
+ "div",
22743
+ {
22744
+ "aria-hidden": "true",
22745
+ className: cn("grid min-w-0 items-start", className),
22746
+ style: { gap, gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))` },
22747
+ children: items.map((item) => /* @__PURE__ */ jsx("div", { children: renderSkeleton(item) }, item.key))
22748
+ }
22749
+ )
22750
+ }
22751
+ );
22752
+ }
22620
22753
  var DEFAULT_GAP_PX = 12;
22621
22754
  var DEFAULT_ROW_HEIGHT_PX = 8;
22622
22755
  var DEFAULT_CARD_WIDTH_PX = 280;
22623
22756
  var BREAKPOINT_TABLET_PX = 640;
22624
22757
  var BREAKPOINT_DESKTOP_PX = 1024;
22625
22758
  var MAX_COLUMN_COUNT = 24;
22626
- function clamp4(value, min5, max5) {
22759
+ function clamp5(value, min5, max5) {
22627
22760
  return Math.min(Math.max(value, min5), max5);
22628
22761
  }
22629
22762
  function resolveRenderableSlot(slot, context) {
@@ -22647,7 +22780,7 @@ function resolveLengthToPixels2(value, fallback, node) {
22647
22780
  }
22648
22781
  function normalizeColumns(value) {
22649
22782
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
22650
- return clamp4(Math.round(value), 1, MAX_COLUMN_COUNT);
22783
+ return clamp5(Math.round(value), 1, MAX_COLUMN_COUNT);
22651
22784
  }
22652
22785
  function resolveResponsiveColumns(columns, containerWidth) {
22653
22786
  if (columns === "auto" || columns === void 0) return void 0;
@@ -22708,7 +22841,7 @@ function normalizeRowHeight(value) {
22708
22841
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_ROW_HEIGHT_PX;
22709
22842
  }
22710
22843
  function normalizeThreshold(value) {
22711
- return typeof value === "number" && Number.isFinite(value) ? clamp4(value, 0, 1) : 0;
22844
+ return typeof value === "number" && Number.isFinite(value) ? clamp5(value, 0, 1) : 0;
22712
22845
  }
22713
22846
  function normalizeItemSize(size, aspectRatio, columnWidth) {
22714
22847
  const normalizedAspectRatio = Number.isFinite(aspectRatio) && Number(aspectRatio) > 0 ? Number(aspectRatio) : void 0;
@@ -22768,7 +22901,7 @@ function warnOnDuplicateKeys(keys) {
22768
22901
  }
22769
22902
  function normalizeTitleMaxLines(value) {
22770
22903
  if (!Number.isFinite(value) || Number(value) <= 0) return void 0;
22771
- return clamp4(Math.round(Number(value)), 1, 12);
22904
+ return clamp5(Math.round(Number(value)), 1, 12);
22772
22905
  }
22773
22906
  function WorkbenchCollection({
22774
22907
  items,
@@ -22818,6 +22951,12 @@ function WorkbenchCollection({
22818
22951
  [normalizedItems]
22819
22952
  );
22820
22953
  const keysSignature = normalizedItems.map(({ stringKey }) => stringKey).join("");
22954
+ const state = config.state;
22955
+ const loading = state?.loading === true;
22956
+ const loadingMore = state?.loadingMore === true;
22957
+ const hasMore = state?.hasMore === true;
22958
+ const loadingVariant = resolveWorkbenchCollectionLoadingVariant(config.loading?.variant);
22959
+ const layoutItemCount = loading && normalizedItems.length === 0 && loadingVariant === "skeleton" ? resolveWorkbenchCollectionSkeletonCapacity(config.loading?.skeleton) : normalizedItems.length;
22821
22960
  useEffect(() => warnOnDuplicateKeys(keys), [keys, keysSignature]);
22822
22961
  const selectionMode = config.selection?.mode ?? "none";
22823
22962
  const controlledSelection = config.selection?.selectedKeys !== void 0;
@@ -22852,7 +22991,7 @@ function WorkbenchCollection({
22852
22991
  }, [itemsNode]);
22853
22992
  const columnCount = resolveColumnCount({
22854
22993
  containerWidth,
22855
- itemCount: normalizedItems.length,
22994
+ itemCount: layoutItemCount,
22856
22995
  gap,
22857
22996
  columns: config.layout.columns,
22858
22997
  maxColumns: config.layout.maxColumns,
@@ -22937,10 +23076,6 @@ function WorkbenchCollection({
22937
23076
  owner.removeEventListener("scroll", saveScrollPosition);
22938
23077
  };
22939
23078
  }, [config.scrollRestorationKey, resolveScrollOwner]);
22940
- const state = config.state;
22941
- const loading = state?.loading === true;
22942
- const loadingMore = state?.loadingMore === true;
22943
- const hasMore = state?.hasMore === true;
22944
23079
  const canLoadMore = typeof callbacks?.onLoadMore === "function";
22945
23080
  const effectiveError = state?.error ?? virtualizationError;
22946
23081
  useEffect(() => {
@@ -23291,7 +23426,7 @@ function WorkbenchCollection({
23291
23426
  },
23292
23427
  children: group.items.map((entry) => {
23293
23428
  const customSize = config.layout.getItemSize?.(entry.item, baseLayoutContext);
23294
- const span = clamp4(Math.round(Number(customSize?.columnSpan) || 1), 1, columnCount);
23429
+ const span = clamp5(Math.round(Number(customSize?.columnSpan) || 1), 1, columnCount);
23295
23430
  const aspectRatio = config.layout.getItemAspectRatio?.(entry.item, entry.index);
23296
23431
  return /* @__PURE__ */ jsx(
23297
23432
  "div",
@@ -23378,7 +23513,28 @@ function WorkbenchCollection({
23378
23513
  title: labels.offline,
23379
23514
  actions: statusStates.error?.showRetry !== false && callbacks?.onRetry ? /* @__PURE__ */ jsx(BaseButton, { size: "sm", label: labels.retry, onClick: callbacks.onRetry }) : void 0
23380
23515
  }
23381
- ) : showInitialLoading ? hasRenderableNode(loadingSlot) ? loadingSlot : /* @__PURE__ */ jsx(BaseLoadingState, { title: labels.loading }) : showInitialError ? hasRenderableNode(errorSlot) ? errorSlot : /* @__PURE__ */ jsx(
23516
+ ) : showInitialLoading ? hasRenderableNode(loadingSlot) ? loadingSlot : loadingVariant === "spinner" ? /* @__PURE__ */ jsx(BaseLoadingState, { title: labels.loading, variant: "spinner" }) : /* @__PURE__ */ jsx(
23517
+ "div",
23518
+ {
23519
+ ref: registerItemsNode,
23520
+ className: cn("min-w-0", classNames?.items),
23521
+ children: /* @__PURE__ */ jsx(
23522
+ WorkbenchCollectionSkeleton,
23523
+ {
23524
+ mode,
23525
+ phase: "initial",
23526
+ columnCount,
23527
+ columnWidth,
23528
+ cardWidth: config.layout.cardWidth ?? config.layout.idealCardWidth,
23529
+ gap,
23530
+ rowHeight,
23531
+ config: config.loading?.skeleton,
23532
+ ariaLabel: labels.loading,
23533
+ className: classNames?.layout
23534
+ }
23535
+ )
23536
+ }
23537
+ ) : showInitialError ? hasRenderableNode(errorSlot) ? errorSlot : /* @__PURE__ */ jsx(
23382
23538
  BaseSystemState,
23383
23539
  {
23384
23540
  kind: "error",
@@ -23422,7 +23578,21 @@ function WorkbenchCollection({
23422
23578
  actions: statusStates.error?.showRetry !== false && callbacks?.onRetry ? /* @__PURE__ */ jsx(BaseButton, { size: "sm", label: labels.retry, onClick: callbacks.onRetry }) : void 0
23423
23579
  }
23424
23580
  ) }) : null,
23425
- items.length > 0 && (hasMore || loadingMore) ? /* @__PURE__ */ jsx("div", { ref: loadMoreRef, className: classNames?.loadMore, "data-collection-load-more": "", children: hasRenderableNode(loadMoreSlot) ? loadMoreSlot : loadingMore ? /* @__PURE__ */ jsx(BaseLoadingState, { title: labels.loadingMore }) : null }) : null,
23581
+ items.length > 0 && (hasMore || loadingMore) ? /* @__PURE__ */ jsx("div", { ref: loadMoreRef, className: classNames?.loadMore, "data-collection-load-more": "", children: hasRenderableNode(loadMoreSlot) ? loadMoreSlot : loadingMore ? loadingVariant === "spinner" ? /* @__PURE__ */ jsx(BaseLoadingState, { title: labels.loadingMore, variant: "spinner" }) : /* @__PURE__ */ jsx(
23582
+ WorkbenchCollectionSkeleton,
23583
+ {
23584
+ mode,
23585
+ phase: "load-more",
23586
+ columnCount,
23587
+ columnWidth,
23588
+ cardWidth: config.layout.cardWidth ?? config.layout.idealCardWidth,
23589
+ gap,
23590
+ rowHeight,
23591
+ config: config.loading?.skeleton,
23592
+ ariaLabel: labels.loadingMore,
23593
+ className: classNames?.layout
23594
+ }
23595
+ ) : null }) : null,
23426
23596
  hasRenderableNode(footer) ? /* @__PURE__ */ jsx("div", { className: classNames?.footer, children: footer }) : null
23427
23597
  ]
23428
23598
  }
@@ -24293,7 +24463,7 @@ var getDomain = (values, min5, max5) => {
24293
24463
  return lower < upper ? [lower, upper] : [upper, lower];
24294
24464
  };
24295
24465
  var scale = (value, domain, start, end) => start + (value - domain[0]) / (domain[1] - domain[0]) * (end - start);
24296
- var clamp5 = (value, min5, max5) => Math.min(max5, Math.max(min5, value));
24466
+ var clamp6 = (value, min5, max5) => Math.min(max5, Math.max(min5, value));
24297
24467
  var activatePoint = (event, point, onPointSelect) => {
24298
24468
  if (!onPointSelect || event.key !== "Enter" && event.key !== " ") return;
24299
24469
  event.preventDefault();
@@ -24375,9 +24545,9 @@ function WorkbenchScatterPlot({
24375
24545
  xThreshold === void 0 ? null : /* @__PURE__ */ jsx(
24376
24546
  "line",
24377
24547
  {
24378
- x1: clamp5(xThreshold, plotLeft, plotRight),
24548
+ x1: clamp6(xThreshold, plotLeft, plotRight),
24379
24549
  y1: plotTop,
24380
- x2: clamp5(xThreshold, plotLeft, plotRight),
24550
+ x2: clamp6(xThreshold, plotLeft, plotRight),
24381
24551
  y2: plotBottom,
24382
24552
  className: "stroke-muted-foreground/50",
24383
24553
  strokeDasharray: "5 5"
@@ -24387,9 +24557,9 @@ function WorkbenchScatterPlot({
24387
24557
  "line",
24388
24558
  {
24389
24559
  x1: plotLeft,
24390
- y1: clamp5(yThreshold, plotTop, plotBottom),
24560
+ y1: clamp6(yThreshold, plotTop, plotBottom),
24391
24561
  x2: plotRight,
24392
- y2: clamp5(yThreshold, plotTop, plotBottom),
24562
+ y2: clamp6(yThreshold, plotTop, plotBottom),
24393
24563
  className: "stroke-muted-foreground/50",
24394
24564
  strokeDasharray: "5 5"
24395
24565
  }
@@ -24416,17 +24586,17 @@ function WorkbenchScatterPlot({
24416
24586
  }
24417
24587
  ),
24418
24588
  validPoints.map((point) => {
24419
- const x = clamp5(
24589
+ const x = clamp6(
24420
24590
  scale(point.x, xDomain, plotLeft, plotRight),
24421
24591
  plotLeft,
24422
24592
  plotRight
24423
24593
  );
24424
- const y = clamp5(
24594
+ const y = clamp6(
24425
24595
  scale(point.y, yDomain, plotBottom, plotTop),
24426
24596
  plotTop,
24427
24597
  plotBottom
24428
24598
  );
24429
- const radius = point.size === void 0 ? 9 : clamp5(scale(point.size, sizeDomain, 7, 18), 7, 18);
24599
+ const radius = point.size === void 0 ? 9 : clamp6(scale(point.size, sizeDomain, 7, 18), 7, 18);
24430
24600
  const selected = selectedPointId === point.id;
24431
24601
  return /* @__PURE__ */ jsxs(
24432
24602
  "g",
@@ -24966,7 +25136,7 @@ var getDomain2 = (values, min5, max5) => {
24966
25136
  return lower < upper ? [lower, upper] : [upper, lower];
24967
25137
  };
24968
25138
  var scale2 = (value, domain, start, end) => start + (value - domain[0]) / (domain[1] - domain[0]) * (end - start);
24969
- var clamp6 = (value, min5, max5) => Math.min(max5, Math.max(min5, value));
25139
+ var clamp7 = (value, min5, max5) => Math.min(max5, Math.max(min5, value));
24970
25140
  var toneVariable = {
24971
25141
  primary: "--radar-series-one",
24972
25142
  secondary: "--radar-series-two",
@@ -25029,7 +25199,7 @@ function WorkbenchRadarMatrix({
25029
25199
  const plotRight = width;
25030
25200
  const plotTop = 0;
25031
25201
  const plotBottom = height;
25032
- const xThreshold = clamp6(
25202
+ const xThreshold = clamp7(
25033
25203
  scale2(
25034
25204
  Number.isFinite(thresholds?.x) ? Number(thresholds?.x) : (xDomain[0] + xDomain[1]) / 2,
25035
25205
  xDomain,
@@ -25039,7 +25209,7 @@ function WorkbenchRadarMatrix({
25039
25209
  plotLeft,
25040
25210
  plotRight
25041
25211
  );
25042
- const yThreshold = clamp6(
25212
+ const yThreshold = clamp7(
25043
25213
  scale2(
25044
25214
  Number.isFinite(thresholds?.y) ? Number(thresholds?.y) : (yDomain[0] + yDomain[1]) / 2,
25045
25215
  yDomain,
@@ -25147,13 +25317,13 @@ function WorkbenchRadarMatrix({
25147
25317
  ] }, quadrant.id);
25148
25318
  }),
25149
25319
  validPoints.map((point, index) => {
25150
- const x = clamp6(scale2(point.x, xDomain, 40, width - 40), 40, width - 40);
25151
- const y = clamp6(scale2(point.y, yDomain, height - 48, 54), 54, height - 48);
25320
+ const x = clamp7(scale2(point.x, xDomain, 40, width - 40), 40, width - 40);
25321
+ const y = clamp7(scale2(point.y, yDomain, height - 48, 54), 54, height - 48);
25152
25322
  const tone = point.tone || (index % 2 === 0 ? "primary" : "accent");
25153
25323
  const selected = selectedPointId === point.id;
25154
25324
  const labelled = labelledPointIds.has(point.id);
25155
- const radius = labelled ? clamp6(scale2(point.size ?? 1, sizeDomain, 5, 9), 5, 9) : 3.5;
25156
- const fontSize = labelled ? clamp6(scale2(point.size ?? 1, sizeDomain, 14, 31), 14, 31) : 0;
25325
+ const radius = labelled ? clamp7(scale2(point.size ?? 1, sizeDomain, 5, 9), 5, 9) : 3.5;
25326
+ const fontSize = labelled ? clamp7(scale2(point.size ?? 1, sizeDomain, 14, 31), 14, 31) : 0;
25157
25327
  const pointStyle = {
25158
25328
  fill: toneColor(tone),
25159
25329
  stroke: "hsl(var(--foreground) / 0.74)"
@@ -25255,8 +25425,8 @@ function WorkbenchRadarMatrix({
25255
25425
  validPoints.map((point, index) => /* @__PURE__ */ jsx(
25256
25426
  "circle",
25257
25427
  {
25258
- cx: clamp6(scale2(point.x, xDomain, 40, width - 40), 40, width - 40),
25259
- cy: clamp6(scale2(point.y, yDomain, height - 48, 54), 54, height - 48),
25428
+ cx: clamp7(scale2(point.x, xDomain, 40, width - 40), 40, width - 40),
25429
+ cy: clamp7(scale2(point.y, yDomain, height - 48, 54), 54, height - 48),
25260
25430
  r: "8",
25261
25431
  style: { fill: toneColor(point.tone || (index % 2 === 0 ? "primary" : "accent")) }
25262
25432
  },
@@ -28612,7 +28782,7 @@ var DEFAULT_ZOOM_STEP = 1.18;
28612
28782
  var DEFAULT_STAGE_PADDING = 16;
28613
28783
  var MAX_SAFE_SCALE = 1e6;
28614
28784
  var MIN_SAFE_SCALE = 1e-6;
28615
- var clamp7 = (value, min5, max5) => Math.min(max5, Math.max(min5, value));
28785
+ var clamp8 = (value, min5, max5) => Math.min(max5, Math.max(min5, value));
28616
28786
  var isNoDragTarget = (target) => target instanceof Element && Boolean(target.closest('[data-no-drag="true"]'));
28617
28787
  function ResetIcon() {
28618
28788
  return /* @__PURE__ */ jsxs(
@@ -28736,7 +28906,7 @@ function DataExplorerImagePreview({
28736
28906
  availableWidth / size.width,
28737
28907
  availableHeight / size.height
28738
28908
  );
28739
- const nextScale = clamp7(
28909
+ const nextScale = clamp8(
28740
28910
  fitScale,
28741
28911
  normalizedMinScale,
28742
28912
  normalizedMaxScale
@@ -28790,7 +28960,7 @@ function DataExplorerImagePreview({
28790
28960
  if (!Number.isFinite(factor) || factor === 1) return;
28791
28961
  if (!Number.isFinite(stageX) || !Number.isFinite(stageY)) return;
28792
28962
  setView((current) => {
28793
- const nextScale = clamp7(
28963
+ const nextScale = clamp8(
28794
28964
  current.scale * factor,
28795
28965
  normalizedMinScale,
28796
28966
  normalizedMaxScale