diff --git a/packages/form-components/CHANGELOG.md b/packages/form-components/CHANGELOG.md index bd7b6e6c..e194a132 100644 --- a/packages/form-components/CHANGELOG.md +++ b/packages/form-components/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.2.3] - 2025-08-08 + +Add LexSelection component + ## [0.2.2] - 2025-06-25 Improve styles and types diff --git a/packages/form-components/package.json b/packages/form-components/package.json index d7e5bed8..7943634e 100644 --- a/packages/form-components/package.json +++ b/packages/form-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/form-components", - "version": "0.2.2", + "version": "0.2.3", "description": "Form components for user input into Macrostrat apps", "type": "module", "source": "src/index.ts", diff --git a/packages/form-components/src/components/lex-selection/index.ts b/packages/form-components/src/components/lex-selection/index.ts new file mode 100644 index 00000000..e0751f1b --- /dev/null +++ b/packages/form-components/src/components/lex-selection/index.ts @@ -0,0 +1,185 @@ +import { Button, MenuItem, MenuItemProps } from "@blueprintjs/core"; +import { ItemPredicate, ItemRenderer, Select2 } from "@blueprintjs/select"; +import { Cell, EditableCell2Props } from "@blueprintjs/table"; +import React, { useMemo, memo } from "react"; +import { useInDarkMode } from "@macrostrat/ui-components"; +import { getColorPair } from "@macrostrat/color-utils"; +import { useAPIResult } from "@macrostrat/ui-components"; +import "@blueprintjs/select/lib/css/blueprint-select.css"; +import h from "@macrostrat/hyper"; + +export interface LexItem { + id: number; + name: string; + color: string; +} + +interface LexSelectionProps extends MenuItemProps { + item: LexItem; + handleClick: (e: React.MouseEvent) => void; + handleFocus: (e: React.FocusEvent) => void; + modifiers: { + active: boolean; + disabled: boolean; + }; +} + +function LexOption({ + item, + handleClick, + handleFocus, + modifiers, + ...restProps +}: LexSelectionProps) { + const inDarkMode = useInDarkMode(); + const colors = getColorPair(item?.color, inDarkMode); + + if (item == null) { + return h( + MenuItem, + { + shouldDismissPopover: true, + active: modifiers.active, + disabled: modifiers.disabled, + key: "", + label: "", + onClick: handleClick, + onFocus: handleFocus, + text: "", + roleStructure: "listoption", + ...restProps, + }, + [], + ); + } + + return h( + MenuItem, + { + style: colors, + shouldDismissPopover: true, + active: modifiers.active, + disabled: modifiers.disabled, + key: item.id, + label: item.id.toString(), + onClick: handleClick, + onFocus: handleFocus, + text: item.name, + roleStructure: "listoption", + ...restProps, + }, + [], + ); +} + +const LexOptionMemo = memo(LexOption); + +const lexOptionRenderer: ItemRenderer = ( + item: LexItem, + props: any, +) => { + return h(LexOptionMemo, { + key: item.id, + item, + ...props, + }); +}; + +const filterInterval: ItemPredicate = (query, item) => { + if (item?.name == undefined) { + return false; + } + return item.name.toLowerCase().indexOf(query.toLowerCase()) >= 0; +}; + +export const LexSelection = ({ + value, + onConfirm, + intent, + items = [], + placeholder = "Select an item", + ...props +}) => { + const [active, setActive] = React.useState(false); + + const item = useMemo(() => { + if (items == null) { + return null; + } + let item = null; + if (items.length != 0) { + item = items.filter((item) => item.id == parseInt(value))[0]; + } + + return item; + }, [value, items, intent]); + + if (items == null) { + return null; + } + + return h( + Cell, + { + ...props, + style: { ...props.style, padding: 0 }, + }, + [ + h( + Select2, + { + fill: true, + items: active ? items : [], + className: "update-input-group", + popoverProps: { + position: "bottom", + minimal: true, + }, + popoverContentProps: { + onWheelCapture: (event) => event.stopPropagation(), + }, + itemPredicate: filterInterval, + itemRenderer: lexOptionRenderer, + onItemSelect: (item: LexItem, e) => { + onConfirm(item.id.toString()); + }, + noResults: h(MenuItem, { + disabled: true, + text: "No results.", + roleStructure: "listoption", + }), + }, + h(LexButton, { item, intent, setActive, placeholder }), + ), + ], + ); +}; + +function LexButton({ item, intent, setActive, placeholder }) { + const inDarkMode = useInDarkMode(); + const colors = getColorPair(item?.color, inDarkMode); + return h( + Button, + { + style: { + ...colors, + fontSize: "12px", + minHeight: "0px", + padding: intent ? "0px 10px" : "1.7px 10px", + boxShadow: "none", + border: intent ? "2px solid green" : "none", + }, + fill: true, + alignText: "left", + text: h( + "span", + { style: { overflow: "hidden", textOverflow: "ellipses" } }, + item?.name ?? placeholder, + ), + rightIcon: "double-caret-vertical", + className: "update-input-group", + onClick: () => setActive(true), + }, + [], + ); +} diff --git a/packages/form-components/src/components/lex-selection/lex-selection.stories.ts b/packages/form-components/src/components/lex-selection/lex-selection.stories.ts new file mode 100644 index 00000000..6f801404 --- /dev/null +++ b/packages/form-components/src/components/lex-selection/lex-selection.stories.ts @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import h from "@macrostrat/hyper"; + +import { LexSelection, LexSelectionProps } from "."; +import { useState } from "react"; +import { useAPIResult } from "@macrostrat/ui-components"; + +// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +export default { + title: "Form components/Lex selection", + component: LexSelection, +} as Meta; + +type Story = StoryObj; + +export function Intervals() { + const [selected, setSelected] = useState(null); + const intervals = useIntervals(); + + if (intervals == null) { + return h("div", {}, "Loading intervals..."); + } + + return h(LexSelection, { + value: selected, + onConfirm: (value) => setSelected(value), + items: intervals, + placeholder: "Select an interval", + }); +} + +function useIntervals() { + return useAPIResult( + "https://dev.macrostrat.org/api/pg/intervals?select=id,color:interval_color,name:interval_name", + ); +} + +export function StratNames() { + const [selected, setSelected] = useState(null); + const StratNames = useStratNames(); + + if (StratNames == null) { + return h("div", "Loading intervals..."); + } + + return h(LexSelection, { + value: selected, + onConfirm: (value) => setSelected(value), + items: StratNames, + placeholder: "Select a strat name", + }); +} + +function useStratNames() { + return useAPIResult( + "https://dev.macrostrat.org/api/pg/strat_names?select=id,name:strat_name", + ); +}