@milktop/react-ui 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -106,7 +106,8 @@ the words of the app's provider.
106
106
  (`classes={{ itemText: 'font-mono' }}`).
107
107
  - Slots replace a part's content: a node, or a function given its state
108
108
  (`indicator={({ indeterminate }) => …}`, `renderItem={(item) => …}`).
109
- - `style={{ '--ui-accent': … }}`: tokens for one instance; on `:root` for the whole app.
109
+ - Tokens for one instance or a section: a class (`className="[--ui-accent:#16a34a]"` with
110
+ Tailwind) or `style={{ '--ui-accent': … }}`; on `:root` for the whole app.
110
111
  - Typed: written in TypeScript, shipped as JS with `.d.ts` types, so TSX apps
111
112
  get checked props (Select's value is the type of your items' `valueKey`)
112
113
  and JSX apps get autocomplete, with no errors either way.
@@ -301,13 +301,14 @@ export function EventCalendar({ events = [], resources = null, resourceViews = n
301
301
  const refocus = useRef(null);
302
302
  const suppressClick = useRef(false);
303
303
  // `byMinutes` kept to the day's hours: an event moves or grows no further than
304
- // the grid shows (one already past an edge doesn't go further past it).
304
+ // the grid shows (one already past an edge doesn't go further past it), and
305
+ // shrinks to a slot at the shortest (one already shorter doesn't shrink).
305
306
  function withinHours(event, kind, byMinutes) {
306
307
  const start = parse(event.start);
307
308
  const from = minutesOfDay(start);
308
- const to = from + minutesBetween(start, parse(event.end));
309
- const latest = Math.max(0, endHour * 60 - to);
310
- const earliest = kind === 'move' ? Math.min(0, startHour * 60 - from) : -Infinity;
309
+ const length = minutesBetween(start, parse(event.end));
310
+ const latest = Math.max(0, endHour * 60 - from - length);
311
+ const earliest = kind === 'move' ? Math.min(0, startHour * 60 - from) : Math.min(0, slotMinutes - length);
311
312
  return Math.max(earliest, Math.min(latest, byMinutes));
312
313
  }
313
314
  // Where a pinned event placed at `minute` starts: on a slot, ending by the day's last hour.
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ export { colorScheme, useColorScheme, type Scheme } from './color-scheme.js';
29
29
  export { Appearance } from './appearance-panel/Appearance.js';
30
30
  export { appearance, useAppearance, accents, accentScale, accentSteps, sidebarStyles, type AccentName, type AppearanceValue, type SidebarStyle, } from './appearance.js';
31
31
  export { Tooltip } from './tooltip/Tooltip.js';
32
- export { Menu, ContextMenu, MenuItem, MenuSeparator, MenuGroup, MenuCheckbox, MenuRadioGroup, MenuRadio, Submenu, type MenuEntry, type MenuOptionChange, } from './menu/Menu.js';
32
+ export { Menu, ContextMenu, MenuItem, MenuSeparator, MenuGroup, MenuCheckbox, MenuRadioGroup, MenuRadio, Submenu, type MenuEntry, type MenuOptionChange, type MenuIndicator, } from './menu/Menu.js';
33
33
  export { Tabs, Tab } from './tabs/Tabs.js';
34
34
  export { Dialog } from './dialog/Dialog.js';
35
35
  export { confirm, type ConfirmOptions } from './confirm/confirm.js';
@@ -29,6 +29,10 @@
29
29
  * component must pass them on, as Button does)
30
30
  * - `placement`: 'bottom-start' (default), 'bottom-end', …
31
31
  * - `label`: a small label above the items
32
+ * - `indicator`: where checkbox and radio items show they're checked: 'end'
33
+ * (default; a tick after the label, so labels line up with the other
34
+ * items) or 'start' (a tick, or a dot for radios, before it); submenus
35
+ * follow their menu's unless they set one
32
36
  * - `keepOpen`: stay open after an item is chosen (to tick several); an
33
37
  * item's own `keepOpen` overrides it
34
38
  * - `context`: open on right-click (or long-press) of the trigger, at the
@@ -48,6 +52,7 @@ import { type Classes } from '../utils.js';
48
52
  import '../theme.css';
49
53
  import './menu.css';
50
54
  type Value = string | number;
55
+ export type MenuIndicator = 'start' | 'end';
51
56
  export type MenuEntry = string | number | {
52
57
  label?: ReactNode;
53
58
  value?: Value;
@@ -157,15 +162,17 @@ declare function Radio({ value, icon, shortcut, disabled, keepOpen, children }:
157
162
  * </Menu.Submenu>
158
163
  *
159
164
  * - `label`: the row's text; `icon`: an optional Iconify name before it
160
- * - `items`: as on Menu; `keepOpen` for its items (it follows its menu)
165
+ * - `items`: as on Menu; `keepOpen` and `indicator` for its items (they
166
+ * follow its menu's)
161
167
  *
162
168
  * Its items' selections reach the outer Menu's `onSelect`.
163
169
  */
164
- declare function Submenu({ label, icon, items, keepOpen, className, children }: {
170
+ declare function Submenu({ label, icon, items, keepOpen, indicator, className, children }: {
165
171
  label: ReactNode;
166
172
  icon?: string | null;
167
173
  items?: MenuEntry[];
168
174
  keepOpen?: boolean | null;
175
+ indicator?: MenuIndicator | null;
169
176
  className?: string;
170
177
  children?: ReactNode;
171
178
  }): import("react").JSX.Element;
@@ -177,6 +184,7 @@ export type MenuProps = Omit<Ark.ContentProps, 'onSelect' | 'onChange'> & {
177
184
  trigger?: ReactElement | null;
178
185
  placement?: NonNullable<Ark.RootProps['positioning']>['placement'];
179
186
  label?: ReactNode;
187
+ indicator?: MenuIndicator;
180
188
  keepOpen?: boolean;
181
189
  context?: boolean;
182
190
  open?: boolean;
@@ -186,7 +194,7 @@ export type MenuProps = Omit<Ark.ContentProps, 'onSelect' | 'onChange'> & {
186
194
  onChange?: (change: MenuOptionChange) => void;
187
195
  classes?: Classes<'positioner' | 'content' | 'menuLabel'>;
188
196
  };
189
- declare function MenuShortcut({ items, labelKey, valueKey, disabledKey, trigger, placement, label, keepOpen, context, open, defaultOpen, onOpenChange, onSelect, onChange, className, classes, children, ...rest }: MenuProps): import("react").JSX.Element;
197
+ declare function MenuShortcut({ items, labelKey, valueKey, disabledKey, trigger, placement, label, indicator, keepOpen, context, open, defaultOpen, onOpenChange, onSelect, onChange, className, classes, children, ...rest }: MenuProps): import("react").JSX.Element;
190
198
  export declare const Menu: typeof MenuShortcut & {
191
199
  Item: typeof Item;
192
200
  Separator: typeof Separator;
package/dist/menu/Menu.js CHANGED
@@ -30,6 +30,10 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
30
30
  * component must pass them on, as Button does)
31
31
  * - `placement`: 'bottom-start' (default), 'bottom-end', …
32
32
  * - `label`: a small label above the items
33
+ * - `indicator`: where checkbox and radio items show they're checked: 'end'
34
+ * (default; a tick after the label, so labels line up with the other
35
+ * items) or 'start' (a tick, or a dot for radios, before it); submenus
36
+ * follow their menu's unless they set one
33
37
  * - `keepOpen`: stay open after an item is chosen (to tick several); an
34
38
  * item's own `keepOpen` overrides it
35
39
  * - `context`: open on right-click (or long-press) of the trigger, at the
@@ -109,7 +113,7 @@ function Checkbox({ checked, defaultChecked = false, onCheckedChange, value, ico
109
113
  const id = useId();
110
114
  const [own, setOwn] = useState(defaultChecked);
111
115
  const on = checked ?? own;
112
- return (_jsxs(Ark.CheckboxItem, { ...menu.part('item'), value: String(value ?? id), checked: on, disabled: !!disabled, ...closeFor(keepOpen), onCheckedChange: (next) => { setOwn(next); onCheckedChange?.(next); }, children: [_jsx("span", { ...menu.part('indicator'), children: _jsx(Ark.ItemIndicator, { ...menu.part('item-indicator'), "data-type": "checkbox", children: _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children })] }));
116
+ return (_jsxs(Ark.CheckboxItem, { ...menu.part('item'), value: String(value ?? id), checked: on, disabled: !!disabled, ...closeFor(keepOpen), onCheckedChange: (next) => { setOwn(next); onCheckedChange?.(next); }, children: [_jsx("span", { ...menu.part('indicator'), "data-side": menu.indicator, children: _jsx(Ark.ItemIndicator, { ...menu.part('item-indicator'), "data-type": "checkbox", children: _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children })] }));
113
117
  }
114
118
  // A radio group's id and value, for its radios.
115
119
  const RadioContext = createContext(null);
@@ -152,7 +156,7 @@ function Radio({ value, icon = null, shortcut = null, disabled = false, keepOpen
152
156
  const values = useContext(RadioValues);
153
157
  const key = `${group?.id}/${value}`;
154
158
  values?.set(key, value);
155
- return (_jsxs(Ark.RadioItem, { ...menu.part('item'), value: key, disabled: !!disabled, ...closeFor(keepOpen), children: [_jsx("span", { ...menu.part('indicator'), children: _jsx(Ark.ItemIndicator, { ...menu.part('item-indicator'), "data-type": "radio" }) }), _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children })] }));
159
+ return (_jsxs(Ark.RadioItem, { ...menu.part('item'), value: key, disabled: !!disabled, ...closeFor(keepOpen), children: [_jsx("span", { ...menu.part('indicator'), "data-side": menu.indicator, children: _jsx(Ark.ItemIndicator, { ...menu.part('item-indicator'), "data-type": "radio", children: menu.indicator === 'end' && _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), _jsx(ItemInside, { part: menu.part, icon: icon, shortcut: shortcut, children: children })] }));
156
160
  }
157
161
  /**
158
162
  * A submenu inside a Menu (or another Submenu): a row that opens a nested
@@ -163,15 +167,17 @@ function Radio({ value, icon = null, shortcut = null, disabled = false, keepOpen
163
167
  * </Menu.Submenu>
164
168
  *
165
169
  * - `label`: the row's text; `icon`: an optional Iconify name before it
166
- * - `items`: as on Menu; `keepOpen` for its items (it follows its menu)
170
+ * - `items`: as on Menu; `keepOpen` and `indicator` for its items (they
171
+ * follow its menu's)
167
172
  *
168
173
  * Its items' selections reach the outer Menu's `onSelect`.
169
174
  */
170
- function Submenu({ label, icon = null, items = [], keepOpen = null, className, children }) {
175
+ function Submenu({ label, icon = null, items = [], keepOpen = null, indicator = null, className, children }) {
171
176
  const outer = useMenu();
172
177
  const part = outer.part;
173
178
  const stays = keepOpen ?? outer.keepOpen;
174
- return (_jsxs(Ark.Root, { closeOnSelect: !stays, positioning: { placement: 'right-start', strategy: 'fixed', gutter: 2 }, navigate: (details) => details.node.click(), onSelect: (details) => outer.select(details.value), children: [_jsxs(Ark.TriggerItem, { ...part('item'), children: [icon && _jsx("iconify-icon", { ...part('icon'), icon: icon, "aria-hidden": "true" }), _jsx("span", { ...part('label'), children: label }), _jsx("iconify-icon", { ...part('chevron'), icon: "lucide:chevron-right", "aria-hidden": "true" })] }), _jsx(Ark.Positioner, { ...part('positioner'), children: _jsx(Ark.Content, { ...part('content'), className: className, children: _jsxs(MenuContext.Provider, { value: { ...outer, keepOpen: stays }, children: [_jsx(Entries, { items: items }), children] }) }) })] }));
179
+ const side = indicator ?? outer.indicator;
180
+ return (_jsxs(Ark.Root, { closeOnSelect: !stays, positioning: { placement: 'right-start', strategy: 'fixed', gutter: 2 }, navigate: (details) => details.node.click(), onSelect: (details) => outer.select(details.value), children: [_jsxs(Ark.TriggerItem, { ...part('item'), children: [icon && _jsx("iconify-icon", { ...part('icon'), icon: icon, "aria-hidden": "true" }), _jsx("span", { ...part('label'), children: label }), _jsx("iconify-icon", { ...part('chevron'), icon: "lucide:chevron-right", "aria-hidden": "true" })] }), _jsx(Ark.Positioner, { ...part('positioner'), children: _jsx(Ark.Content, { ...part('content'), className: className, children: _jsxs(MenuContext.Provider, { value: { ...outer, keepOpen: stays, indicator: side }, children: [_jsx(Entries, { items: items }), children] }) }) })] }));
175
181
  }
176
182
  // A checkbox or radio entry from `items`, checked by the root menu. Zag's
177
183
  // option props straight onto it: Ark's RadioItem wants a RadioItemGroup round it.
@@ -183,7 +189,7 @@ function Option({ entry, optionKey, label, disabled, siblings }) {
183
189
  valueText: typeof label === 'string' ? label : optionKey, ...closeFor(entry.keepOpen),
184
190
  onCheckedChange: (on) => menu.check(entry, optionKey, on, siblings),
185
191
  };
186
- return (_jsxs("div", { ...api.getOptionItemProps(props), ...menu.part('item'), "data-danger": entry.danger || undefined, children: [_jsx("span", { ...menu.part('indicator'), children: _jsx("span", { ...api.getItemIndicatorProps(props), ...menu.part('item-indicator'), "data-type": entry.type, children: entry.type === 'checkbox' && _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), entry.icon && _jsx("iconify-icon", { ...menu.part('icon'), icon: entry.icon, "aria-hidden": "true" }), _jsx("span", { ...menu.part('label'), children: label }), entry.shortcut && _jsx("kbd", { ...menu.part('shortcut'), children: entry.shortcut })] }));
192
+ return (_jsxs("div", { ...api.getOptionItemProps(props), ...menu.part('item'), "data-danger": entry.danger || undefined, children: [_jsx("span", { ...menu.part('indicator'), "data-side": menu.indicator, children: _jsx("span", { ...api.getItemIndicatorProps(props), ...menu.part('item-indicator'), "data-type": entry.type, children: (entry.type === 'checkbox' || menu.indicator === 'end') && _jsx("iconify-icon", { icon: "lucide:check", "aria-hidden": "true" }) }) }), entry.icon && _jsx("iconify-icon", { ...menu.part('icon'), icon: entry.icon, "aria-hidden": "true" }), _jsx("span", { ...menu.part('label'), children: label }), entry.shortcut && _jsx("kbd", { ...menu.part('shortcut'), children: entry.shortcut })] }));
187
193
  }
188
194
  // A menu's `items` array, as the components above.
189
195
  function Entries({ items }) {
@@ -210,13 +216,13 @@ function Entries({ items }) {
210
216
  return (_jsx(Item, { value: value, href: entry.href, icon: entry.icon, shortcut: entry.shortcut, danger: entry.danger, disabled: disabled, keepOpen: entry.keepOpen, children: label }, i));
211
217
  }) });
212
218
  }
213
- function MenuShortcut({ items = [], labelKey = 'label', valueKey = 'value', disabledKey = 'disabled', trigger = null, placement = 'bottom-start', label = null, keepOpen = false, context = false, open, defaultOpen, onOpenChange, onSelect, onChange, className, classes, children, ...rest }) {
219
+ function MenuShortcut({ items = [], labelKey = 'label', valueKey = 'value', disabledKey = 'disabled', trigger = null, placement = 'bottom-start', label = null, indicator = 'end', keepOpen = false, context = false, open, defaultOpen, onOpenChange, onSelect, onChange, className, classes, children, ...rest }) {
214
220
  const part = partsOf('menu', classes);
215
221
  const values = useRef(new Map());
216
222
  // `items` checkbox and radio entries, starting from their own `checked`.
217
223
  const [checks, setChecks] = useState({});
218
224
  const shared = {
219
- part, keepOpen, keys: { labelKey, valueKey, disabledKey },
225
+ part, keepOpen, indicator, keys: { labelKey, valueKey, disabledKey },
220
226
  remember: (key, value) => { values.current.set(key, value); },
221
227
  select: (key) => { if (values.current.has(key))
222
228
  onSelect?.(values.current.get(key)); },
@@ -28,6 +28,9 @@
28
28
  [data-scope=menu][data-part=item-indicator] {
29
29
  &[data-type=checkbox] { display: block; font-size: 14px; color: var(--ui-accent); }
30
30
  &[data-type=radio] { display: block; width: 6px; height: 6px; border-radius: 9999px; background: var(--ui-accent); box-shadow: 0 0 0 3px var(--ui-accent-soft); } }
31
+ /* indicator='end' (the default): a tick after the label and any shortcut, for radios too. */
32
+ [data-scope=menu][data-part=indicator][data-side=end] { order: 1;
33
+ & > [data-type=radio] { width: auto; height: auto; font-size: 14px; color: var(--ui-accent); border-radius: 0; background: none; box-shadow: none; } }
31
34
  [data-scope=menu][data-part=chevron] { display: block; font-size: 14px; margin-left: auto; color: var(--ui-muted); }
32
35
  /* A div or an <hr> (Ark's): no border of its own. */
33
36
  [data-scope=menu][data-part=separator] { display: block; height: 1px; margin: 0.25rem -0.25rem; border: none; background: var(--ui-border); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milktop/react-ui",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "React components on Ark UI (Zag state machines), with imba-ui's API and design",
5
5
  "license": "MIT",
6
6
  "author": "André Goldstein",