aplance-ui 1.5.0 → 1.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aplance-ui",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "The aplance-ui CLI - scaffold the design system and copy components into your project as source.",
5
5
  "keywords": [
6
6
  "react",
@@ -30,6 +30,10 @@
30
30
  min-width: 0;
31
31
  }
32
32
 
33
+ .rail_without_controls {
34
+ --scroll-rail-control: 0px;
35
+ }
36
+
33
37
  /* The scroller. It shows no scrollbar, so the row never changes height, and
34
38
  pads like a toolbar so its controls' focus rings show, since a scroller
35
39
  clips both axes. */
@@ -88,17 +92,57 @@
88
92
  }
89
93
  }
90
94
 
91
- /* A chevron, over the clear edge of its fade, on the row's middle. */
92
- .more {
95
+ /* A chevron, over the clear edge of its fade, on the row's middle. The
96
+ primitive owns only neutral button chrome; native props let a caller replace
97
+ the icon, label, classes, and inline styles without depending on Button. */
98
+ .control {
93
99
  position: absolute;
94
100
  top: 50%;
95
101
  translate: 0 -50%;
102
+ z-index: 1;
103
+ display: inline-flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: var(--spacing-10);
107
+ height: var(--spacing-10);
108
+ padding: 0;
109
+ appearance: none;
110
+ font: inherit;
111
+ color: var(--color-foreground);
112
+ background-color: var(--color-background);
113
+ border: 1px solid var(--color-border);
114
+ border-radius: var(--radius-md);
115
+ box-shadow: var(--shadow-sm);
116
+ cursor: pointer;
117
+ transition:
118
+ color var(--transition-base),
119
+ background-color var(--transition-base),
120
+ border-color var(--transition-base),
121
+ opacity var(--transition-base);
122
+
123
+ &:hover:not(:disabled) {
124
+ background-color: var(--color-muted);
125
+ }
126
+
127
+ &:focus-visible {
128
+ outline: 2px solid var(--color-primary);
129
+ outline-offset: 2px;
130
+ }
131
+
132
+ &:disabled {
133
+ cursor: not-allowed;
134
+ opacity: 0.5;
135
+ }
136
+
137
+ > svg {
138
+ pointer-events: none;
139
+ }
96
140
  }
97
141
 
98
- .more_start {
142
+ .control_start {
99
143
  left: 0;
100
144
  }
101
145
 
102
- .more_end {
146
+ .control_end {
103
147
  right: 0;
104
148
  }
@@ -3,13 +3,13 @@ import {
3
3
  useEffect,
4
4
  useRef,
5
5
  useState,
6
+ type ComponentPropsWithoutRef,
6
7
  type ReactNode,
7
8
  type Ref,
8
9
  } from "react";
9
10
  import clsx from "clsx";
10
11
  import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
11
12
  import { useResizeObserver } from "../../../hooks/useResizeObserver";
12
- import { Button } from "../button/button";
13
13
  import s from "./scroll-rail.module.css";
14
14
 
15
15
  // A row that scrolls sideways, as a row of chips or tabs does on a phone:
@@ -30,12 +30,36 @@ import s from "./scroll-rail.module.css";
30
30
  /** How far a chevron scrolls, in widths of the row. */
31
31
  const PAGE = 0.8;
32
32
 
33
- export type ScrollRailProps = {
33
+ export type ScrollRailControlProps = Omit<
34
+ ComponentPropsWithoutRef<"button">,
35
+ "type"
36
+ >;
37
+
38
+ export type ScrollRailControls = {
39
+ /** Class shared by both arrow buttons. */
40
+ className?: string;
41
+ /** Native button props for the previous arrow. `children` replaces its icon. */
42
+ previous?: ScrollRailControlProps;
43
+ /** Native button props for the next arrow. `children` replaces its icon. */
44
+ next?: ScrollRailControlProps;
45
+ };
46
+
47
+ export type ScrollRailProps = Omit<
48
+ ComponentPropsWithoutRef<"div">,
49
+ "children"
50
+ > & {
34
51
  children: ReactNode;
35
52
  /** The rail's own box, as a flex item where it sits. */
36
53
  className?: string;
54
+ /** Class merged onto the horizontal scroller. */
55
+ trackClassName?: string;
37
56
  /** The row's layout around its items, such as their gap. */
38
57
  contentClassName?: string;
58
+ /**
59
+ * Customize the overflow arrows with native button props, or hide them with
60
+ * `false`. A custom onClick runs first and can prevent the built-in scroll.
61
+ */
62
+ controls?: false | ScrollRailControls;
39
63
  /** The scroller, for a caller that scrolls it itself, as Tabs keeps its
40
64
  * active tab in view. */
41
65
  ref?: Ref<HTMLDivElement>;
@@ -54,8 +78,11 @@ const wheelUnit = (event: WheelEvent, track: HTMLElement) =>
54
78
  export const ScrollRail = ({
55
79
  children,
56
80
  className,
81
+ trackClassName,
57
82
  contentClassName,
83
+ controls,
58
84
  ref,
85
+ ...rootProps
59
86
  }: ScrollRailProps) => {
60
87
  const trackRef = useRef<HTMLDivElement | null>(null);
61
88
  const setTrack = useCallback(
@@ -85,6 +112,12 @@ export const ScrollRail = ({
85
112
  useResizeObserver(trackRef, measure);
86
113
  useResizeObserver(contentRef, measure);
87
114
 
115
+ // Measure once after mount as well. ResizeObserver reports later size
116
+ // changes, but its initial notification is not synchronous in every host.
117
+ useEffect(() => {
118
+ measure();
119
+ }, [measure]);
120
+
88
121
  useEffect(() => {
89
122
  const track = trackRef.current;
90
123
  if (!track) return;
@@ -114,46 +147,68 @@ export const ScrollRail = ({
114
147
  });
115
148
  };
116
149
 
150
+ const renderControl = (direction: -1 | 1) => {
151
+ const visible = direction === -1 ? ends.start : ends.end;
152
+ if (!visible || controls === false) return null;
153
+
154
+ const control = direction === -1 ? controls?.previous : controls?.next;
155
+ const {
156
+ children: controlIcon,
157
+ className: controlClassName,
158
+ onClick,
159
+ "aria-label": ariaLabel,
160
+ ...controlProps
161
+ } = control ?? {};
162
+ const label = direction === -1 ? "Scroll left" : "Scroll right";
163
+
164
+ return (
165
+ <button
166
+ {...controlProps}
167
+ type="button"
168
+ className={clsx(
169
+ s.control,
170
+ direction === -1 ? s.control_start : s.control_end,
171
+ controls?.className,
172
+ controlClassName,
173
+ )}
174
+ aria-label={ariaLabel ?? label}
175
+ onClick={(event) => {
176
+ onClick?.(event);
177
+ if (!event.defaultPrevented) page(direction);
178
+ }}
179
+ >
180
+ {controlIcon ??
181
+ (direction === -1 ? (
182
+ <IconChevronLeft size={14} stroke={2} />
183
+ ) : (
184
+ <IconChevronRight size={14} stroke={2} />
185
+ ))}
186
+ </button>
187
+ );
188
+ };
189
+
117
190
  return (
118
191
  <div
192
+ {...rootProps}
119
193
  className={clsx(
120
194
  s.rail,
121
195
  ends.start && s.rail_start,
122
196
  ends.end && s.rail_end,
197
+ controls === false && s.rail_without_controls,
123
198
  className,
124
199
  )}
125
200
  >
126
- <div ref={setTrack} className={s.track} onScroll={measure}>
201
+ <div
202
+ ref={setTrack}
203
+ className={clsx(s.track, trackClassName)}
204
+ onScroll={measure}
205
+ >
127
206
  <div ref={contentRef} className={clsx(s.content, contentClassName)}>
128
207
  {children}
129
208
  </div>
130
209
  </div>
131
- {ends.start ? (
132
- <Button
133
- type="button"
134
- variant="bordered"
135
- size="sm"
136
- layout="box"
137
- className={clsx(s.more, s.more_start)}
138
- aria-label="Scroll left"
139
- onClick={() => page(-1)}
140
- >
141
- <IconChevronLeft size={14} stroke={2} />
142
- </Button>
143
- ) : null}
144
- {ends.end ? (
145
- <Button
146
- type="button"
147
- variant="bordered"
148
- size="sm"
149
- layout="box"
150
- className={clsx(s.more, s.more_end)}
151
- aria-label="Scroll right"
152
- onClick={() => page(1)}
153
- >
154
- <IconChevronRight size={14} stroke={2} />
155
- </Button>
156
- ) : null}
210
+ {renderControl(-1)}
211
+ {renderControl(1)}
157
212
  </div>
158
213
  );
159
214
  };
@@ -8,7 +8,7 @@ import clsx from "clsx";
8
8
  import { Button } from "../button/button";
9
9
  import { CustomSelect } from "../input-fields/custom-select/custom-select";
10
10
  import { Loader } from "../loader/loader";
11
- import { ScrollRail } from "../scroll-rail/scroll-rail";
11
+ import { ScrollRail } from "../../primitives/scroll-rail/scroll-rail";
12
12
  import type { PaginationBarProps } from "./types";
13
13
  import s from "./data-table.module.css";
14
14
 
@@ -10,7 +10,7 @@ import {
10
10
  } from "react";
11
11
  import clsx from "clsx";
12
12
  import { useResizeObserver } from "../../../hooks/useResizeObserver";
13
- import { ScrollRail } from "../scroll-rail/scroll-rail";
13
+ import { ScrollRail } from "../../primitives/scroll-rail/scroll-rail";
14
14
  import s from "./tabs.module.css";
15
15
 
16
16
  // Variants & Types
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://ui.aplance.com/registry.schema.json",
3
3
  "version": 4,
4
- "generated": "2026-11-11T06:11:34.254Z",
4
+ "generated": "2026-11-11T12:39:14.314Z",
5
5
  "units": [
6
6
  {
7
7
  "name": "accordion",
@@ -573,16 +573,15 @@
573
573
  {
574
574
  "name": "scroll-rail",
575
575
  "kind": "component",
576
- "group": "ui",
576
+ "group": "primitives",
577
577
  "files": [
578
- "components/ui/scroll-rail/scroll-rail.module.css",
579
- "components/ui/scroll-rail/scroll-rail.tsx"
578
+ "components/primitives/scroll-rail/scroll-rail.module.css",
579
+ "components/primitives/scroll-rail/scroll-rail.tsx"
580
580
  ],
581
581
  "clientFiles": [
582
- "components/ui/scroll-rail/scroll-rail.tsx"
582
+ "components/primitives/scroll-rail/scroll-rail.tsx"
583
583
  ],
584
584
  "deps": [
585
- "button",
586
585
  "useResizeObserver"
587
586
  ],
588
587
  "externalDeps": [