@wheelhouse/ui 0.2.6 → 0.2.8
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/blocks/columns/column-popover-panel-header.d.ts +10 -0
- package/dist/blocks/columns/column-popover-panel-header.d.ts.map +1 -0
- package/dist/blocks/columns/column-popover-panel-header.js +9 -0
- package/dist/blocks/columns/columns-add-view.d.ts +12 -0
- package/dist/blocks/columns/columns-add-view.d.ts.map +1 -0
- package/dist/blocks/columns/columns-add-view.js +34 -0
- package/dist/blocks/columns/columns-types.d.ts +26 -1
- package/dist/blocks/columns/columns-types.d.ts.map +1 -1
- package/dist/blocks/columns/columns-types.js +9 -2
- package/dist/blocks/columns/columns-utils.d.ts +7 -3
- package/dist/blocks/columns/columns-utils.d.ts.map +1 -1
- package/dist/blocks/columns/columns-utils.js +28 -6
- package/dist/blocks/columns/columns.d.ts.map +1 -1
- package/dist/blocks/columns/columns.js +106 -68
- package/dist/blocks/columns/columns.stories.d.ts +1 -0
- package/dist/blocks/columns/columns.stories.d.ts.map +1 -1
- package/dist/blocks/columns/columns.stories.js +19 -4
- package/dist/blocks/columns/index.d.ts +1 -1
- package/dist/blocks/columns/index.d.ts.map +1 -1
- package/dist/blocks/columns/index.js +1 -1
- package/dist/components/avatar/avatar.d.ts +3 -2
- package/dist/components/avatar/avatar.d.ts.map +1 -1
- package/dist/components/avatar/avatar.js +3 -2
- package/dist/components/avatar/avatar.stories.d.ts.map +1 -1
- package/dist/components/avatar/avatar.stories.js +7 -0
- package/dist/components/button/button.d.ts +3 -3
- package/dist/components/button/button.js +7 -7
- package/dist/components/button-group/button-group.js +2 -2
- package/dist/components/calendar/calendar.js +2 -2
- package/dist/components/data-grid/data-grid-column-filter.js +1 -1
- package/dist/components/field/field.d.ts +1 -1
- package/dist/components/field/field.d.ts.map +1 -1
- package/dist/components/filters/filter-date-metric-value.js +1 -1
- package/dist/components/filters/filters.d.ts.map +1 -1
- package/dist/components/filters/filters.js +3 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/input/input.d.ts +2 -2
- package/dist/components/input/input.d.ts.map +1 -1
- package/dist/components/input/input.js +4 -3
- package/dist/components/input/input.stories.d.ts +1 -1
- package/dist/components/input/input.stories.d.ts.map +1 -1
- package/dist/components/input/input.stories.js +1 -1
- package/dist/components/input-group/input-group.d.ts +1 -1
- package/dist/components/number-field/index.d.ts +2 -0
- package/dist/components/number-field/index.d.ts.map +1 -0
- package/dist/components/number-field/index.js +1 -0
- package/dist/components/number-field/number-field.d.ts +59 -0
- package/dist/components/number-field/number-field.d.ts.map +1 -0
- package/dist/components/number-field/number-field.js +49 -0
- package/dist/components/number-field/number-field.stories.d.ts +25 -0
- package/dist/components/number-field/number-field.stories.d.ts.map +1 -0
- package/dist/components/number-field/number-field.stories.js +225 -0
- package/dist/components/overlapping-stack/index.d.ts +3 -0
- package/dist/components/overlapping-stack/index.d.ts.map +1 -0
- package/dist/components/overlapping-stack/index.js +2 -0
- package/dist/components/overlapping-stack/overlapping-stack.d.ts +12 -0
- package/dist/components/overlapping-stack/overlapping-stack.d.ts.map +1 -0
- package/dist/components/overlapping-stack/overlapping-stack.js +45 -0
- package/dist/components/overlapping-stack/overlapping-stack.stories.d.ts +78 -0
- package/dist/components/overlapping-stack/overlapping-stack.stories.d.ts.map +1 -0
- package/dist/components/overlapping-stack/overlapping-stack.stories.js +120 -0
- package/dist/components/overlapping-stack/use-overlapping-stack.d.ts +47 -0
- package/dist/components/overlapping-stack/use-overlapping-stack.d.ts.map +1 -0
- package/dist/components/overlapping-stack/use-overlapping-stack.js +47 -0
- package/dist/components/sidebar/sidebar.js +1 -1
- package/dist/components/textarea/textarea.js +1 -1
- package/dist/components/toggle/toggle.d.ts +3 -3
- package/dist/components/toggle/toggle.js +4 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/styles/globals.css +6 -24
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { AtSignIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
|
3
|
+
import { Field } from '../field';
|
|
4
|
+
import { Label } from '../label';
|
|
5
|
+
import { NumberField, NumberFieldAddon, NumberFieldDecrement, NumberFieldGroup, NumberFieldIncrement, NumberFieldInput, NumberFieldScrubArea, } from './number-field';
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/Number field',
|
|
8
|
+
component: NumberField,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'centered',
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
size: 'default',
|
|
14
|
+
defaultValue: 0,
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
size: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['sm', 'default', 'lg'],
|
|
20
|
+
description: 'Control height. Read by children through `data-size`.',
|
|
21
|
+
table: { type: { summary: "'sm' | 'default' | 'lg'" }, defaultValue: { summary: "'default'" } },
|
|
22
|
+
},
|
|
23
|
+
value: {
|
|
24
|
+
control: 'number',
|
|
25
|
+
description: 'Controlled numeric value. Use `null` to represent an empty field.',
|
|
26
|
+
table: { type: { summary: 'number | null' }, category: 'Value' },
|
|
27
|
+
},
|
|
28
|
+
defaultValue: {
|
|
29
|
+
control: 'number',
|
|
30
|
+
description: 'Initial numeric value when uncontrolled.',
|
|
31
|
+
table: { type: { summary: 'number' }, category: 'Value' },
|
|
32
|
+
},
|
|
33
|
+
onValueChange: {
|
|
34
|
+
action: 'valueChange',
|
|
35
|
+
description: 'Called when the value changes, with the parsed number (or `null` when empty) and event details describing the change reason.',
|
|
36
|
+
table: { type: { summary: '(value: number | null, eventDetails) => void' }, category: 'Value' },
|
|
37
|
+
},
|
|
38
|
+
onValueCommitted: {
|
|
39
|
+
action: 'valueCommitted',
|
|
40
|
+
description: 'Called when the user finishes interacting — on blur, after release of increment/decrement, or after the scrub gesture ends.',
|
|
41
|
+
table: { type: { summary: '(value: number | null, eventDetails) => void' }, category: 'Value' },
|
|
42
|
+
},
|
|
43
|
+
min: {
|
|
44
|
+
control: 'number',
|
|
45
|
+
description: 'Minimum allowed value.',
|
|
46
|
+
table: { type: { summary: 'number' }, category: 'Range' },
|
|
47
|
+
},
|
|
48
|
+
max: {
|
|
49
|
+
control: 'number',
|
|
50
|
+
description: 'Maximum allowed value.',
|
|
51
|
+
table: { type: { summary: 'number' }, category: 'Range' },
|
|
52
|
+
},
|
|
53
|
+
step: {
|
|
54
|
+
control: 'number',
|
|
55
|
+
description: 'Amount to increment/decrement with buttons and arrow keys. Set to `"any"` to disable native step validation.',
|
|
56
|
+
table: { type: { summary: "number | 'any'" }, defaultValue: { summary: '1' }, category: 'Range' },
|
|
57
|
+
},
|
|
58
|
+
smallStep: {
|
|
59
|
+
control: 'number',
|
|
60
|
+
description: 'Step used when the meta (or Ctrl) modifier key is held.',
|
|
61
|
+
table: { type: { summary: 'number' }, defaultValue: { summary: '0.1' }, category: 'Range' },
|
|
62
|
+
},
|
|
63
|
+
largeStep: {
|
|
64
|
+
control: 'number',
|
|
65
|
+
description: 'Step used when the Shift modifier key is held.',
|
|
66
|
+
table: { type: { summary: 'number' }, defaultValue: { summary: '10' }, category: 'Range' },
|
|
67
|
+
},
|
|
68
|
+
snapOnStep: {
|
|
69
|
+
control: 'boolean',
|
|
70
|
+
description: 'Snap to the nearest multiple of `step` when incrementing or decrementing.',
|
|
71
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'Range' },
|
|
72
|
+
},
|
|
73
|
+
allowOutOfRange: {
|
|
74
|
+
control: 'boolean',
|
|
75
|
+
description: 'Allow typed values outside `min`/`max` (step-based interactions still clamp).',
|
|
76
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'Range' },
|
|
77
|
+
},
|
|
78
|
+
allowWheelScrub: {
|
|
79
|
+
control: 'boolean',
|
|
80
|
+
description: 'Allow the mouse wheel to change the value while the input is focused and hovered.',
|
|
81
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'Range' },
|
|
82
|
+
},
|
|
83
|
+
format: {
|
|
84
|
+
control: 'object',
|
|
85
|
+
description: 'Locale-aware formatting options passed to `Intl.NumberFormat` (e.g. currency, percent, decimal grouping).',
|
|
86
|
+
table: { type: { summary: 'Intl.NumberFormatOptions' }, category: 'Formatting' },
|
|
87
|
+
},
|
|
88
|
+
locale: {
|
|
89
|
+
control: 'text',
|
|
90
|
+
description: 'Locale used for formatting. Defaults to the user’s runtime locale.',
|
|
91
|
+
table: { type: { summary: 'Intl.LocalesArgument' }, category: 'Formatting' },
|
|
92
|
+
},
|
|
93
|
+
disabled: {
|
|
94
|
+
control: 'boolean',
|
|
95
|
+
description: 'Whether the control ignores interaction.',
|
|
96
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' },
|
|
97
|
+
},
|
|
98
|
+
readOnly: {
|
|
99
|
+
control: 'boolean',
|
|
100
|
+
description: 'Whether the user can read but not change the value.',
|
|
101
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' },
|
|
102
|
+
},
|
|
103
|
+
required: {
|
|
104
|
+
control: 'boolean',
|
|
105
|
+
description: 'Whether a value is required before submitting a form.',
|
|
106
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' },
|
|
107
|
+
},
|
|
108
|
+
name: {
|
|
109
|
+
control: 'text',
|
|
110
|
+
description: 'Name submitted with the surrounding form.',
|
|
111
|
+
table: { type: { summary: 'string' }, category: 'Form' },
|
|
112
|
+
},
|
|
113
|
+
id: {
|
|
114
|
+
control: 'text',
|
|
115
|
+
description: 'ID applied to the inner input element. Use with `Label`’s `htmlFor`.',
|
|
116
|
+
table: { type: { summary: 'string' }, category: 'Form' },
|
|
117
|
+
},
|
|
118
|
+
inputRef: {
|
|
119
|
+
control: false,
|
|
120
|
+
description: 'Ref forwarded to the inner `<input>` element — useful for focusing, measuring, or imperative selection.',
|
|
121
|
+
table: { type: { summary: 'Ref<HTMLInputElement>' }, category: 'Form' },
|
|
122
|
+
},
|
|
123
|
+
className: { control: false, table: { type: { summary: 'string' } } },
|
|
124
|
+
children: { control: false, table: { type: { summary: 'ReactNode' } } },
|
|
125
|
+
},
|
|
126
|
+
decorators: [
|
|
127
|
+
(Story) => (_jsx("div", { className: "flex w-64 flex-col gap-2", children: _jsx(Story, {}) })),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
export default meta;
|
|
131
|
+
export const Default = {
|
|
132
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-default", children: "Quantity" }), _jsx(NumberField, { ...args, id: "story-number-default", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })),
|
|
133
|
+
};
|
|
134
|
+
export const Sizes = {
|
|
135
|
+
parameters: { layout: 'padded' },
|
|
136
|
+
decorators: [(Story) => _jsx(Story, {})],
|
|
137
|
+
render: () => (_jsxs("div", { className: "flex w-full max-w-xs flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-number-sm", children: "Small" }), _jsx(NumberField, { id: "story-number-sm", size: "sm", defaultValue: 5, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-number-default-size", children: "Default" }), _jsx(NumberField, { id: "story-number-default-size", defaultValue: 5, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-number-lg", children: "Large" }), _jsx(NumberField, { id: "story-number-lg", size: "lg", defaultValue: 5, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })] })),
|
|
138
|
+
};
|
|
139
|
+
export const MinMaxStep = {
|
|
140
|
+
name: 'Min / max / step',
|
|
141
|
+
args: { defaultValue: 50, min: 0, max: 100, step: 5 },
|
|
142
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-step", children: "Volume (0\u2013100, step 5)" }), _jsx(NumberField, { ...args, id: "story-number-step", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })),
|
|
143
|
+
};
|
|
144
|
+
export const Currency = {
|
|
145
|
+
name: 'Currency format',
|
|
146
|
+
args: { defaultValue: 19.99, step: 0.5, format: { style: 'currency', currency: 'USD' } },
|
|
147
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-currency", children: "Price" }), _jsx(NumberField, { ...args, id: "story-number-currency", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })),
|
|
148
|
+
};
|
|
149
|
+
export const Percent = {
|
|
150
|
+
name: 'Percent format',
|
|
151
|
+
args: { defaultValue: 0.25, step: 0.05, format: { style: 'percent' } },
|
|
152
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-percent", children: "Discount" }), _jsx(NumberField, { ...args, id: "story-number-percent", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })),
|
|
153
|
+
};
|
|
154
|
+
export const PrefixAddon = {
|
|
155
|
+
name: 'Prefix addon',
|
|
156
|
+
args: { defaultValue: 19.99, step: 0.5 },
|
|
157
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-prefix", children: "Price" }), _jsx(NumberField, { ...args, id: "story-number-prefix", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldAddon, { children: "$" }), _jsx(NumberFieldInput, { className: "text-start" })] }) })] })),
|
|
158
|
+
};
|
|
159
|
+
export const SuffixAddon = {
|
|
160
|
+
name: 'Suffix addon',
|
|
161
|
+
args: { defaultValue: 25, step: 1, min: 0, max: 100 },
|
|
162
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-suffix", children: "Discount" }), _jsx(NumberField, { ...args, id: "story-number-suffix", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-end" }), _jsx(NumberFieldAddon, { align: "inline-end", children: "%" })] }) })] })),
|
|
163
|
+
};
|
|
164
|
+
export const UnitAddon = {
|
|
165
|
+
name: 'Unit / label addon',
|
|
166
|
+
args: { defaultValue: 5, min: 0, step: 0.5 },
|
|
167
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-unit", children: "Weight" }), _jsx(NumberField, { ...args, id: "story-number-unit", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-end" }), _jsx(NumberFieldAddon, { align: "inline-end", children: "kg" })] }) })] })),
|
|
168
|
+
};
|
|
169
|
+
export const IconAddon = {
|
|
170
|
+
name: 'Icon addon',
|
|
171
|
+
args: { defaultValue: 0, min: 0 },
|
|
172
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-icon", children: "Mentions" }), _jsx(NumberField, { ...args, id: "story-number-icon", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldAddon, { children: _jsx(AtSignIcon, { className: "size-3" }) }), _jsx(NumberFieldInput, { className: "text-start" })] }) })] })),
|
|
173
|
+
};
|
|
174
|
+
export const AddonWithStepper = {
|
|
175
|
+
name: 'Addon + stepper',
|
|
176
|
+
args: { defaultValue: 19.99, step: 0.5, min: 0 },
|
|
177
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-addon-stepper", children: "Price" }), _jsx(NumberField, { ...args, id: "story-number-addon-stepper", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldAddon, { children: "$" }), _jsx(NumberFieldInput, { className: "text-start" }), _jsx(NumberFieldDecrement, { className: "rounded-none border-l border-input" }), _jsx(NumberFieldIncrement, { className: "border-l border-input" })] }) })] })),
|
|
178
|
+
};
|
|
179
|
+
export const StepperRight = {
|
|
180
|
+
name: 'Stepper buttons on right',
|
|
181
|
+
args: { defaultValue: 1, min: 0 },
|
|
182
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-stepper-right", children: "Quantity" }), _jsx(NumberField, { ...args, id: "story-number-stepper-right", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-start" }), _jsx(NumberFieldDecrement, { className: "rounded-none border-l border-input" }), _jsx(NumberFieldIncrement, { className: "border-l border-input" })] }) })] })),
|
|
183
|
+
};
|
|
184
|
+
export const StackedStepper = {
|
|
185
|
+
name: 'Stacked stepper (chevrons)',
|
|
186
|
+
args: { defaultValue: 1, min: 0 },
|
|
187
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-stacked", children: "Quantity" }), _jsx(NumberField, { ...args, id: "story-number-stacked", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-start" }), _jsxs("div", { className: "flex flex-col border-l border-input", children: [_jsx(NumberFieldIncrement, { className: "flex-1 rounded-none rounded-tr-[calc(var(--radius-lg)-1px)] border-b border-input px-2", children: _jsx(ChevronUpIcon, { className: "size-3" }) }), _jsx(NumberFieldDecrement, { className: "flex-1 rounded-none rounded-br-[calc(var(--radius-lg)-1px)] px-2", children: _jsx(ChevronDownIcon, { className: "size-3" }) })] })] }) })] })),
|
|
188
|
+
};
|
|
189
|
+
export const SteppersWithSuffixHorizontal = {
|
|
190
|
+
name: 'Steppers on right (horizontal) with suffix',
|
|
191
|
+
parameters: { layout: 'padded' },
|
|
192
|
+
decorators: [(Story) => _jsx(Story, {})],
|
|
193
|
+
render: () => (_jsxs("div", { className: "flex w-full max-w-xs flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-num-rightH-nights", children: "Stay length" }), _jsx(NumberField, { id: "story-num-rightH-nights", defaultValue: 3, min: 1, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-end" }), _jsx(NumberFieldAddon, { align: "inline-end", className: "order-none rounded-none", children: "nights" }), _jsx(NumberFieldDecrement, { className: "rounded-none border-l border-input" }), _jsx(NumberFieldIncrement, { className: "border-l border-input" })] }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-num-rightH-pct", children: "Tip" }), _jsx(NumberField, { id: "story-num-rightH-pct", defaultValue: 15, min: 0, max: 100, step: 1, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-end" }), _jsx(NumberFieldAddon, { align: "inline-end", className: "order-none rounded-none", children: "%" }), _jsx(NumberFieldDecrement, { className: "rounded-none border-l border-input" }), _jsx(NumberFieldIncrement, { className: "border-l border-input" })] }) })] })] })),
|
|
194
|
+
};
|
|
195
|
+
export const SteppersWithSuffixStacked = {
|
|
196
|
+
name: 'Steppers on right (stacked) with suffix',
|
|
197
|
+
parameters: { layout: 'padded' },
|
|
198
|
+
decorators: [(Story) => _jsx(Story, {})],
|
|
199
|
+
render: () => (_jsxs("div", { className: "flex w-full max-w-xs flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-num-rightS-nights", children: "Stay length" }), _jsx(NumberField, { id: "story-num-rightS-nights", defaultValue: 3, min: 1, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-end" }), _jsx(NumberFieldAddon, { align: "inline-end", className: "order-none rounded-none", children: "nights" }), _jsxs("div", { className: "flex flex-col border-l border-input", children: [_jsx(NumberFieldIncrement, { className: "flex-1 rounded-none rounded-tr-[calc(var(--radius-lg)-1px)] border-b border-input px-2", children: _jsx(ChevronUpIcon, { className: "size-3" }) }), _jsx(NumberFieldDecrement, { className: "flex-1 rounded-none rounded-br-[calc(var(--radius-lg)-1px)] px-2", children: _jsx(ChevronDownIcon, { className: "size-3" }) })] })] }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "story-num-rightS-pct", children: "Tip" }), _jsx(NumberField, { id: "story-num-rightS-pct", defaultValue: 15, min: 0, max: 100, step: 1, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldInput, { className: "text-end" }), _jsx(NumberFieldAddon, { align: "inline-end", className: "order-none rounded-none", children: "%" }), _jsxs("div", { className: "flex flex-col border-l border-input", children: [_jsx(NumberFieldIncrement, { className: "flex-1 rounded-none rounded-tr-[calc(var(--radius-lg)-1px)] border-b border-input px-2", children: _jsx(ChevronUpIcon, { className: "size-3" }) }), _jsx(NumberFieldDecrement, { className: "flex-1 rounded-none rounded-br-[calc(var(--radius-lg)-1px)] px-2", children: _jsx(ChevronDownIcon, { className: "size-3" }) })] })] }) })] })] })),
|
|
200
|
+
};
|
|
201
|
+
export const ScrubArea = {
|
|
202
|
+
name: 'Scrub area',
|
|
203
|
+
args: { defaultValue: 100 },
|
|
204
|
+
render: (args) => (_jsxs(NumberField, { ...args, id: "story-number-scrub", children: [_jsx(NumberFieldScrubArea, { label: "Width" }), _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] })] })),
|
|
205
|
+
};
|
|
206
|
+
export const Disabled = {
|
|
207
|
+
args: { defaultValue: 10, disabled: true },
|
|
208
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-disabled", children: "Quantity" }), _jsx(NumberField, { ...args, id: "story-number-disabled", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })),
|
|
209
|
+
};
|
|
210
|
+
export const ReadOnly = {
|
|
211
|
+
name: 'Read only',
|
|
212
|
+
args: { defaultValue: 42, readOnly: true },
|
|
213
|
+
render: (args) => (_jsxs(_Fragment, { children: [_jsx(Label, { htmlFor: "story-number-readonly", children: "Locked value" }), _jsx(NumberField, { ...args, id: "story-number-readonly", children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })),
|
|
214
|
+
};
|
|
215
|
+
export const WithField = {
|
|
216
|
+
name: 'With Field (validation)',
|
|
217
|
+
parameters: { layout: 'padded' },
|
|
218
|
+
decorators: [(Story) => _jsx(Story, {})],
|
|
219
|
+
render: () => (_jsxs("div", { className: "flex w-full max-w-sm flex-col gap-6", children: [_jsxs(Field.Root, { name: "quantity", invalid: true, children: [_jsx(Field.Label, { children: "Quantity" }), _jsx(NumberField, { defaultValue: -1, min: 0, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) }), _jsx(Field.Error, { children: "Must be 0 or greater." })] }), _jsxs(Field.Root, { name: "seats", disabled: true, children: [_jsx(Field.Label, { children: "Seats" }), _jsx(NumberField, { defaultValue: 2, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })] })),
|
|
220
|
+
};
|
|
221
|
+
export const Gallery = {
|
|
222
|
+
parameters: { layout: 'padded', controls: { disable: true } },
|
|
223
|
+
decorators: [(Story) => _jsx(Story, {})],
|
|
224
|
+
render: () => (_jsxs("div", { className: "mx-auto flex w-full max-w-md flex-col gap-10", children: [_jsxs("section", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Default" }), _jsx(Label, { htmlFor: "gal-num-1", children: "Items" }), _jsx(NumberField, { id: "gal-num-1", defaultValue: 1, min: 0, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] }), _jsxs("section", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Currency" }), _jsx(Label, { htmlFor: "gal-num-2", children: "Price" }), _jsx(NumberField, { id: "gal-num-2", defaultValue: 19.99, step: 0.5, format: { style: 'currency', currency: 'USD' }, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] }), _jsxs("section", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "With scrub area" }), _jsxs(NumberField, { id: "gal-num-3", defaultValue: 120, children: [_jsx(NumberFieldScrubArea, { label: "Width" }), _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] })] })] }), _jsxs("section", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: "Disabled" }), _jsx(Label, { htmlFor: "gal-num-4", children: "Not available" }), _jsx(NumberField, { id: "gal-num-4", defaultValue: 3, disabled: true, children: _jsxs(NumberFieldGroup, { children: [_jsx(NumberFieldDecrement, {}), _jsx(NumberFieldInput, {}), _jsx(NumberFieldIncrement, {})] }) })] })] })),
|
|
225
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { OverlappingStack, type OverlappingStackDirection, type OverlappingStackProps } from './overlapping-stack';
|
|
2
|
+
export { useOverlappingStack, type OverlappingStackItemProps, type OverlappingStackMode, type UseOverlappingStackOptions, type UseOverlappingStackResult, } from './use-overlapping-stack';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/overlapping-stack/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAE,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACnH,OAAO,EACH,mBAAmB,EACnB,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,GACjC,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type OverlappingStackDirection, type UseOverlappingStackOptions } from './use-overlapping-stack';
|
|
3
|
+
export type OverlappingStackProps = Omit<React.ComponentProps<'div'>, 'children'> & Omit<UseOverlappingStackOptions, 'count'> & {
|
|
4
|
+
/** Override auto-derived child count. */
|
|
5
|
+
count?: number;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
/** Root classes used only in inline mode (e.g. tighter gap). */
|
|
8
|
+
inlineClassName?: string;
|
|
9
|
+
};
|
|
10
|
+
declare function OverlappingStack({ children, count: countProp, collapseThreshold, overlap, spread, spreadOnHover, overlapDirection, spreadDirection, inlineClassName, className, style, ...props }: OverlappingStackProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export { OverlappingStack, type OverlappingStackDirection };
|
|
12
|
+
//# sourceMappingURL=overlapping-stack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlapping-stack.d.ts","sourceRoot":"","sources":["../../../src/components/overlapping-stack/overlapping-stack.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAE,KAAK,yBAAyB,EAAuB,KAAK,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAoB/H,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,GAC7E,IAAI,CAAC,0BAA0B,EAAE,OAAO,CAAC,GAAG;IACxC,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,gEAAgE;IAChE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEN,iBAAS,gBAAgB,CAAC,EACtB,QAAQ,EACR,KAAK,EAAE,SAAS,EAChB,iBAAiB,EACjB,OAAO,EACP,MAAM,EACN,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACX,EAAE,qBAAqB,2CAuCvB;AAED,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { useOverlappingStack } from './use-overlapping-stack';
|
|
6
|
+
function flattenValidElements(children) {
|
|
7
|
+
const result = [];
|
|
8
|
+
React.Children.forEach(children, (child) => {
|
|
9
|
+
if (!React.isValidElement(child))
|
|
10
|
+
return;
|
|
11
|
+
if (child.type === React.Fragment) {
|
|
12
|
+
const { children: fragmentChildren } = child.props;
|
|
13
|
+
result.push(...flattenValidElements(fragmentChildren));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
result.push(child);
|
|
17
|
+
});
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function OverlappingStack({ children, count: countProp, collapseThreshold, overlap, spread, spreadOnHover, overlapDirection, spreadDirection, inlineClassName, className, style, ...props }) {
|
|
21
|
+
const childArray = flattenValidElements(children);
|
|
22
|
+
const { mode, rootProps, getItemProps } = useOverlappingStack({
|
|
23
|
+
count: countProp ?? childArray.length,
|
|
24
|
+
collapseThreshold,
|
|
25
|
+
overlap,
|
|
26
|
+
spread,
|
|
27
|
+
spreadOnHover,
|
|
28
|
+
overlapDirection,
|
|
29
|
+
spreadDirection,
|
|
30
|
+
});
|
|
31
|
+
const isInline = mode === 'inline';
|
|
32
|
+
return (_jsx("div", { "data-slot": rootProps['data-slot'], "data-mode": rootProps['data-mode'], ...props, className: cn(isInline && 'flex w-fit items-center gap-1', isInline ? inlineClassName : rootProps.className, className), style: isInline ? style : { ...rootProps.style, ...style }, children: isInline
|
|
33
|
+
? children
|
|
34
|
+
: childArray.map((child, index) => {
|
|
35
|
+
const { className: itemClassName, style: itemStyle, ...itemProps } = getItemProps(index);
|
|
36
|
+
const childElement = child;
|
|
37
|
+
return React.cloneElement(childElement, {
|
|
38
|
+
key: childElement.key ?? index,
|
|
39
|
+
...itemProps,
|
|
40
|
+
className: cn(itemClassName, childElement.props.className),
|
|
41
|
+
style: { ...itemStyle, ...childElement.props.style },
|
|
42
|
+
});
|
|
43
|
+
}) }));
|
|
44
|
+
}
|
|
45
|
+
export { OverlappingStack };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react';
|
|
2
|
+
import { OverlappingStack } from './overlapping-stack';
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof OverlappingStack;
|
|
6
|
+
tags: string[];
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: string;
|
|
9
|
+
options: {
|
|
10
|
+
storySort: {
|
|
11
|
+
order: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
argTypes: {
|
|
16
|
+
count: {
|
|
17
|
+
control: false;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
children: {
|
|
21
|
+
control: false;
|
|
22
|
+
};
|
|
23
|
+
inlineClassName: {
|
|
24
|
+
control: false;
|
|
25
|
+
};
|
|
26
|
+
className: {
|
|
27
|
+
control: false;
|
|
28
|
+
};
|
|
29
|
+
spreadOnHover: {
|
|
30
|
+
control: "boolean";
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
spread: {
|
|
34
|
+
control: {
|
|
35
|
+
type: "number";
|
|
36
|
+
min: number;
|
|
37
|
+
max: number;
|
|
38
|
+
step: number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
overlap: {
|
|
42
|
+
control: {
|
|
43
|
+
type: "number";
|
|
44
|
+
min: number;
|
|
45
|
+
max: number;
|
|
46
|
+
step: number;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
collapseThreshold: {
|
|
50
|
+
control: {
|
|
51
|
+
type: "number";
|
|
52
|
+
min: number;
|
|
53
|
+
max: number;
|
|
54
|
+
step: number;
|
|
55
|
+
};
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
58
|
+
overlapDirection: {
|
|
59
|
+
control: "inline-radio";
|
|
60
|
+
options: string[];
|
|
61
|
+
};
|
|
62
|
+
spreadDirection: {
|
|
63
|
+
control: "inline-radio";
|
|
64
|
+
options: string[];
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
export default meta;
|
|
69
|
+
type Story = StoryObj<typeof meta>;
|
|
70
|
+
export declare const StackedThreeSpreadOnHover: Story;
|
|
71
|
+
/** Two items with default collapseThreshold (2) — inline row; spreadOnHover has no effect. */
|
|
72
|
+
export declare const InlineTwo: Story;
|
|
73
|
+
export declare const StackedThreeStatic: Story;
|
|
74
|
+
export declare const ExpandOnHoverComparison: Story;
|
|
75
|
+
export declare const OverlapDirectionComparison: Story;
|
|
76
|
+
export declare const RtlSpread: Story;
|
|
77
|
+
export declare const WithAvatarGroup: Story;
|
|
78
|
+
//# sourceMappingURL=overlapping-stack.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlapping-stack.stories.d.ts","sourceRoot":"","sources":["../../../src/components/overlapping-stack/overlapping-stack.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAAE,gBAAgB,EAA8B,MAAM,qBAAqB,CAAC;AAQnF,QAAA,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuC+B,CAAC;AAE1C,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA8BnC,eAAO,MAAM,yBAAyB,EAAE,KAQvC,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,SAAS,EAAE,KAOvB,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,KAOhC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,KA6BrC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,KAqDxC,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAkBvB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAkB7B,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from '../avatar';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { OverlappingStack } from './overlapping-stack';
|
|
5
|
+
/** Storybook may infer a `count` control; passing it overrides child length and can force inline mode. */
|
|
6
|
+
function stackArgs(args) {
|
|
7
|
+
const { children: _children, count: _count, ...stackArgs } = args;
|
|
8
|
+
return stackArgs;
|
|
9
|
+
}
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'Components/OverlappingStack',
|
|
12
|
+
component: OverlappingStack,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'centered',
|
|
16
|
+
options: {
|
|
17
|
+
storySort: {
|
|
18
|
+
order: [
|
|
19
|
+
'Stacked Three Spread On Hover',
|
|
20
|
+
'Expand On Hover Comparison',
|
|
21
|
+
'Inline Two',
|
|
22
|
+
'Stacked Three Static',
|
|
23
|
+
'Overlap Direction Comparison',
|
|
24
|
+
'Rtl Spread',
|
|
25
|
+
'With Avatar Group',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
argTypes: {
|
|
31
|
+
count: { control: false, description: 'Derived from children in stories; do not set via Controls.' },
|
|
32
|
+
children: { control: false },
|
|
33
|
+
inlineClassName: { control: false },
|
|
34
|
+
className: { control: false },
|
|
35
|
+
spreadOnHover: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Fan stacked items apart on group hover and focus-within (stacked mode only).',
|
|
38
|
+
},
|
|
39
|
+
spread: { control: { type: 'number', min: 0, max: 24, step: 1 } },
|
|
40
|
+
overlap: { control: { type: 'number', min: 0, max: 24, step: 1 } },
|
|
41
|
+
collapseThreshold: {
|
|
42
|
+
control: { type: 'number', min: 0, max: 6, step: 1 },
|
|
43
|
+
description: 'Inline row when child count ≤ this value. overlap/spread only apply in stacked mode. Keep ≤ 2 for three-marker stories; AvatarGroup uses 0.',
|
|
44
|
+
},
|
|
45
|
+
overlapDirection: { control: 'inline-radio', options: ['ltr', 'rtl'] },
|
|
46
|
+
spreadDirection: { control: 'inline-radio', options: ['ltr', 'rtl'] },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export default meta;
|
|
50
|
+
const AVATAR_SRC = 'https://avatars.githubusercontent.com/u/1406596?v=4';
|
|
51
|
+
/** Must forward `className`, `style`, and data attrs — OverlappingStack merges stack props via cloneElement. */
|
|
52
|
+
function MarkerIcon({ label, className, style, ...props }) {
|
|
53
|
+
return (_jsx("button", { type: "button", "aria-label": label, className: cn('inline-flex size-10 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground ring-2 ring-background', className), style: style, ...props, children: label.slice(0, 1) }));
|
|
54
|
+
}
|
|
55
|
+
const threeMarkers = (_jsxs(_Fragment, { children: [_jsx(MarkerIcon, { label: "Booking A" }), _jsx(MarkerIcon, { label: "Booking B" }), _jsx(MarkerIcon, { label: "Housekeeping" })] }));
|
|
56
|
+
export const StackedThreeSpreadOnHover = {
|
|
57
|
+
args: {
|
|
58
|
+
spreadOnHover: true,
|
|
59
|
+
overlap: 8,
|
|
60
|
+
spread: 6,
|
|
61
|
+
collapseThreshold: 2,
|
|
62
|
+
},
|
|
63
|
+
render: (args) => _jsx(OverlappingStack, { ...stackArgs(args), children: threeMarkers }),
|
|
64
|
+
};
|
|
65
|
+
/** Two items with default collapseThreshold (2) — inline row; spreadOnHover has no effect. */
|
|
66
|
+
export const InlineTwo = {
|
|
67
|
+
render: () => (_jsxs(OverlappingStack, { children: [_jsx(MarkerIcon, { label: "Rate override" }), _jsx(MarkerIcon, { label: "Min stay" })] })),
|
|
68
|
+
};
|
|
69
|
+
export const StackedThreeStatic = {
|
|
70
|
+
args: {
|
|
71
|
+
spreadOnHover: false,
|
|
72
|
+
overlap: 8,
|
|
73
|
+
collapseThreshold: 2,
|
|
74
|
+
},
|
|
75
|
+
render: (args) => _jsx(OverlappingStack, { ...stackArgs(args), children: threeMarkers }),
|
|
76
|
+
};
|
|
77
|
+
export const ExpandOnHoverComparison = {
|
|
78
|
+
args: {
|
|
79
|
+
spreadOnHover: true,
|
|
80
|
+
overlap: 8,
|
|
81
|
+
spread: 6,
|
|
82
|
+
collapseThreshold: 2,
|
|
83
|
+
},
|
|
84
|
+
render: (args) => {
|
|
85
|
+
const stack = stackArgs(args);
|
|
86
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "spreadOnHover (hover to expand)" }), _jsx(OverlappingStack, { ...stack, children: threeMarkers })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "spreadOnHover=false (overlap only)" }), _jsx(OverlappingStack, { spreadOnHover: false, overlap: stack.overlap, collapseThreshold: stack.collapseThreshold, overlapDirection: stack.overlapDirection, children: threeMarkers })] })] }));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
export const OverlapDirectionComparison = {
|
|
90
|
+
args: {
|
|
91
|
+
spreadOnHover: true,
|
|
92
|
+
overlap: 8,
|
|
93
|
+
spread: 6,
|
|
94
|
+
collapseThreshold: 2,
|
|
95
|
+
},
|
|
96
|
+
render: (args) => {
|
|
97
|
+
const { overlap, spread, collapseThreshold, spreadOnHover } = stackArgs(args);
|
|
98
|
+
return (_jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "overlapDirection=ltr" }), _jsx(OverlappingStack, { overlapDirection: "ltr", spreadDirection: "ltr", spreadOnHover: spreadOnHover, overlap: overlap, spread: spread, collapseThreshold: collapseThreshold, children: threeMarkers })] }), _jsxs("div", { className: "flex w-48 flex-col gap-2 self-end border border-dashed border-border p-4", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "overlap ltr + spread rtl (end-aligned)" }), _jsx(OverlappingStack, { overlapDirection: "ltr", spreadDirection: "rtl", spreadOnHover: spreadOnHover, overlap: overlap, spread: spread, collapseThreshold: collapseThreshold, children: threeMarkers })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "overlapDirection=rtl" }), _jsx(OverlappingStack, { overlapDirection: "rtl", spreadDirection: "rtl", spreadOnHover: spreadOnHover, overlap: overlap, spread: spread, collapseThreshold: collapseThreshold, children: threeMarkers })] })] }));
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
export const RtlSpread = {
|
|
102
|
+
args: {
|
|
103
|
+
spreadOnHover: true,
|
|
104
|
+
overlap: 6,
|
|
105
|
+
spread: 4,
|
|
106
|
+
collapseThreshold: 2,
|
|
107
|
+
overlapDirection: 'ltr',
|
|
108
|
+
spreadDirection: 'rtl',
|
|
109
|
+
},
|
|
110
|
+
render: (args) => (_jsx("div", { className: "flex w-48 justify-end border border-dashed border-border p-4", children: _jsxs(OverlappingStack, { ...stackArgs(args), children: [_jsx(MarkerIcon, { label: "Flag" }), _jsx(MarkerIcon, { label: "Gap night" }), _jsx(MarkerIcon, { label: "Promo" })] }) })),
|
|
111
|
+
};
|
|
112
|
+
export const WithAvatarGroup = {
|
|
113
|
+
args: {
|
|
114
|
+
spreadOnHover: true,
|
|
115
|
+
overlap: 8,
|
|
116
|
+
spread: 6,
|
|
117
|
+
collapseThreshold: 0,
|
|
118
|
+
},
|
|
119
|
+
render: (args) => (_jsxs(AvatarGroup, { ...stackArgs(args), children: [['SC', 'JD', 'AB'].map((fallback) => (_jsxs(Avatar, { children: [_jsx(AvatarImage, { src: fallback === 'SC' ? AVATAR_SRC : '/broken.jpg', alt: fallback }), _jsx(AvatarFallback, { children: fallback })] }, fallback))), _jsx(AvatarGroupCount, { children: "+4" })] })),
|
|
120
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type OverlappingStackDirection = 'ltr' | 'rtl';
|
|
3
|
+
export type OverlappingStackMode = 'inline' | 'stacked';
|
|
4
|
+
export type UseOverlappingStackOptions = {
|
|
5
|
+
/** Number of stacked items. */
|
|
6
|
+
count: number;
|
|
7
|
+
/** Use inline row layout when `count` is at or below this value. */
|
|
8
|
+
collapseThreshold?: number;
|
|
9
|
+
/** Pixels each item overlaps the previous one (stacked mode). */
|
|
10
|
+
overlap?: number;
|
|
11
|
+
/** Pixels each item moves on group hover/focus per index (stacked mode, when `spreadOnHover`). */
|
|
12
|
+
spread?: number;
|
|
13
|
+
/**
|
|
14
|
+
* When `true`, stacked items fan apart on group hover and `focus-within`.
|
|
15
|
+
* Has no effect in inline mode (when `count` is at or below `collapseThreshold`).
|
|
16
|
+
*/
|
|
17
|
+
spreadOnHover?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Which way each item overlaps the previous one.
|
|
20
|
+
* `ltr`: negative `margin-inline-start`. `rtl`: negative `margin-inline-end` + reversed flex order.
|
|
21
|
+
* @default 'ltr'
|
|
22
|
+
*/
|
|
23
|
+
overlapDirection?: OverlappingStackDirection;
|
|
24
|
+
/**
|
|
25
|
+
* Which way items fan on hover/focus. Defaults to `overlapDirection`.
|
|
26
|
+
*/
|
|
27
|
+
spreadDirection?: OverlappingStackDirection;
|
|
28
|
+
};
|
|
29
|
+
export type OverlappingStackItemProps = {
|
|
30
|
+
/** Stack item marker; does not replace the child's own `data-slot`. */
|
|
31
|
+
'data-overlapping-stack-item': true;
|
|
32
|
+
'data-stack-index': number;
|
|
33
|
+
className?: string;
|
|
34
|
+
style?: React.CSSProperties;
|
|
35
|
+
};
|
|
36
|
+
export type UseOverlappingStackResult = {
|
|
37
|
+
mode: OverlappingStackMode;
|
|
38
|
+
rootProps: {
|
|
39
|
+
'data-slot': 'overlapping-stack';
|
|
40
|
+
'data-mode': OverlappingStackMode;
|
|
41
|
+
className?: string;
|
|
42
|
+
style?: React.CSSProperties;
|
|
43
|
+
};
|
|
44
|
+
getItemProps: (index: number) => OverlappingStackItemProps;
|
|
45
|
+
};
|
|
46
|
+
export declare function useOverlappingStack({ count, collapseThreshold, overlap, spread, spreadOnHover, overlapDirection: overlapDirectionProp, spreadDirection: spreadDirectionProp, }: UseOverlappingStackOptions): UseOverlappingStackResult;
|
|
47
|
+
//# sourceMappingURL=use-overlapping-stack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-overlapping-stack.d.ts","sourceRoot":"","sources":["../../../src/components/overlapping-stack/use-overlapping-stack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,MAAM,yBAAyB,GAAG,KAAK,GAAG,KAAK,CAAC;AAEtD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAExD,MAAM,MAAM,0BAA0B,GAAG;IACrC,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,yBAAyB,CAAC;IAC7C;;OAEG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACpC,uEAAuE;IACvE,6BAA6B,EAAE,IAAI,CAAC;IACpC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACpC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,SAAS,EAAE;QACP,WAAW,EAAE,mBAAmB,CAAC;QACjC,WAAW,EAAE,oBAAoB,CAAC;QAClC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;KAC/B,CAAC;IACF,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,yBAAyB,CAAC;CAC9D,CAAC;AAmBF,wBAAgB,mBAAmB,CAAC,EAChC,KAAK,EACL,iBAAqB,EACrB,OAAW,EACX,MAAU,EACV,aAAqB,EACrB,gBAAgB,EAAE,oBAA4B,EAC9C,eAAe,EAAE,mBAAmB,GACvC,EAAE,0BAA0B,GAAG,yBAAyB,CAuCxD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
const STACKED_ITEM_Z = 'z-[var(--stack-z)]';
|
|
4
|
+
const STACKED_ITEM_BASE = 'relative shrink-0 transition-transform duration-200 ease-out group-hover/stack:z-10 group-hover/stack:[&:hover]:z-20 group-focus-within/stack:z-10 group-focus-within/stack:[&:focus-within]:z-20';
|
|
5
|
+
/** Use transform, not Tailwind translate-x — v4 translate-x sets `translate: var(--tw-translate-x) var(--tw-translate-y)` and --tw-translate-y is unset without translate-y-0, invalidating the whole declaration. */
|
|
6
|
+
const SPREAD_LTR = 'group-hover/stack:[transform:translateX(calc(var(--stack-index)*var(--spread)))] group-focus-within/stack:[transform:translateX(calc(var(--stack-index)*var(--spread)))]';
|
|
7
|
+
const SPREAD_RTL = 'group-hover/stack:[transform:translateX(calc(var(--stack-index)*var(--spread)*-1))] group-focus-within/stack:[transform:translateX(calc(var(--stack-index)*var(--spread)*-1))]';
|
|
8
|
+
function getOverlapMargin(overlapDirection, overlap, index) {
|
|
9
|
+
if (index === 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
return overlapDirection === 'ltr' ? { marginInlineStart: -overlap } : { marginInlineEnd: -overlap };
|
|
12
|
+
}
|
|
13
|
+
export function useOverlappingStack({ count, collapseThreshold = 2, overlap = 8, spread = 6, spreadOnHover = false, overlapDirection: overlapDirectionProp = 'ltr', spreadDirection: spreadDirectionProp, }) {
|
|
14
|
+
const overlapDirection = overlapDirectionProp;
|
|
15
|
+
const spreadDirection = spreadDirectionProp ?? overlapDirection;
|
|
16
|
+
const mode = count <= collapseThreshold ? 'inline' : 'stacked';
|
|
17
|
+
const rootProps = mode === 'inline'
|
|
18
|
+
? {
|
|
19
|
+
'data-slot': 'overlapping-stack',
|
|
20
|
+
'data-mode': 'inline',
|
|
21
|
+
}
|
|
22
|
+
: {
|
|
23
|
+
'data-slot': 'overlapping-stack',
|
|
24
|
+
'data-mode': 'stacked',
|
|
25
|
+
className: cn('group/stack flex w-fit items-center', overlapDirection === 'rtl' && 'flex-row-reverse'),
|
|
26
|
+
style: spreadOnHover ? { ['--spread']: `${spread}px` } : undefined,
|
|
27
|
+
};
|
|
28
|
+
function getItemProps(index) {
|
|
29
|
+
if (mode === 'inline') {
|
|
30
|
+
return {
|
|
31
|
+
'data-overlapping-stack-item': true,
|
|
32
|
+
'data-stack-index': index,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
'data-overlapping-stack-item': true,
|
|
37
|
+
'data-stack-index': index,
|
|
38
|
+
style: {
|
|
39
|
+
['--stack-z']: count - index,
|
|
40
|
+
...getOverlapMargin(overlapDirection, overlap, index),
|
|
41
|
+
...(spreadOnHover ? { ['--stack-index']: String(index) } : undefined),
|
|
42
|
+
},
|
|
43
|
+
className: cn(STACKED_ITEM_Z, STACKED_ITEM_BASE, spreadOnHover && (spreadDirection === 'rtl' ? SPREAD_RTL : SPREAD_LTR)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { mode, rootProps, getItemProps };
|
|
47
|
+
}
|
|
@@ -111,7 +111,7 @@ function SidebarInset({ className, ...props }) {
|
|
|
111
111
|
return (_jsx("main", { "data-slot": "sidebar-inset", className: cn('relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2', className), ...props }));
|
|
112
112
|
}
|
|
113
113
|
function SidebarInput({ className, ...props }) {
|
|
114
|
-
return _jsx(Input, { "data-slot": "sidebar-input", "data-sidebar": "input", className: cn('
|
|
114
|
+
return _jsx(Input, { "data-slot": "sidebar-input", "data-sidebar": "input", className: cn('w-full bg-background shadow-none', className), ...props });
|
|
115
115
|
}
|
|
116
116
|
function SidebarHeader({ className, ...props }) {
|
|
117
117
|
return _jsx("div", { "data-slot": "sidebar-header", "data-sidebar": "header", className: cn('flex flex-col gap-2 p-2', className), ...props });
|