diff --git a/pkgs/clan-app/ui/src/components/Form/Combobox.tsx b/pkgs/clan-app/ui/src/components/Form/Combobox.tsx index ce0f08923..dfee47b92 100644 --- a/pkgs/clan-app/ui/src/components/Form/Combobox.tsx +++ b/pkgs/clan-app/ui/src/components/Form/Combobox.tsx @@ -12,12 +12,15 @@ import cx from "classnames"; import { FieldProps } from "./Field"; import { Orienter } from "./Orienter"; import { Typography } from "@/src/components/Typography/Typography"; -import { Accessor, Component, For, Show, splitProps } from "solid-js"; +import { Accessor, Component, ComponentProps, For, Show, splitProps } from "solid-js"; import { Tag } from "@/src/components/Tag/Tag"; +import { PolymorphicProps } from "@kobalte/core/polymorphic"; +import { TextFieldInputProps } from "@kobalte/core/text-field"; export type ComboboxProps = FieldProps & KComboboxRootOptions & { inverted: boolean; + input?: ComponentProps<"select">; itemControl?: Component>; }; @@ -129,6 +132,7 @@ export const Combobox = ( {...props} /> + class="control"> {(state) => { const [controlProps] = splitProps(props, [ diff --git a/pkgs/clan-app/ui/src/components/Form/MachineTags.css b/pkgs/clan-app/ui/src/components/Form/MachineTags.css new file mode 100644 index 000000000..f4a20711b --- /dev/null +++ b/pkgs/clan-app/ui/src/components/Form/MachineTags.css @@ -0,0 +1,222 @@ +div.form-field.machine-tags { + + div.control { + @apply flex flex-col size-full gap-2; + + div.selected-options { + @apply flex flex-wrap gap-2 size-full min-h-5; + } + + div.input-container { + @apply relative left-0 top-0; + @apply inline-flex justify-between w-full; + + input { + @apply w-full px-2 py-1.5 rounded-sm; + @apply outline outline-1 outline-def-acc-1 bg-def-1 fg-def-1; + + font-size: 0.875rem; + font-weight: 500; + font-family: "Archivo", sans-serif; + line-height: 1; + + &::placeholder { + @apply fg-def-4; + } + + &:hover { + @apply bg-def-acc-1 outline-def-acc-2; + } + + &:focus-visible { + @apply bg-def-1 outline-def-acc-3; + + box-shadow: + 0 0 0 0.125rem theme(colors.bg.def.1), + 0 0 0 0.1875rem theme(colors.border.semantic.info.1); + } + + &[data-invalid] { + @apply outline-semantic-error-4; + } + + &[data-disabled] { + @apply outline-def-2 fg-def-4 cursor-not-allowed; + } + + &[data-readonly] { + @apply outline-none border-none bg-inherit; + @apply p-0 resize-none; + } + } + + & > button.trigger { + @apply flex items-center justify-center w-8; + @apply absolute right-2 top-1 h-5 w-6 bg-def-2 rounded-sm; + + &[data-disabled] { + @apply cursor-not-allowed; + } + + & > span.icon { + @apply h-full w-full py-0.5 px-1; + } + } + } + } + + &.horizontal { + @apply flex-row gap-2 justify-between; + + div.control { + @apply w-1/2 grow; + } + } + + &.s { + div.control > div.input-container { + & > input { + @apply px-1.5 py-1; + font-size: 0.75rem; + + &[data-readonly] { + @apply p-0; + } + } + + & > button.trigger { + @apply top-[0.1875rem] h-4 w-5; + } + } + } + + &.inverted { + div.control > div.input-container { + & > button.trigger { + @apply bg-inv-2; + } + + & > input { + @apply bg-inv-1 fg-inv-1 outline-inv-acc-1; + + &::placeholder { + @apply fg-inv-4; + } + + &:hover { + @apply bg-inv-acc-2 outline-inv-acc-2; + } + + &:focus-visible { + @apply bg-inv-acc-4; + box-shadow: + 0 0 0 0.125rem theme(colors.bg.inv.1), + 0 0 0 0.1875rem theme(colors.border.semantic.info.1); + } + + &[data-invalid] { + @apply outline-semantic-error-4; + } + + &[data-readonly] { + @apply outline-none border-none bg-inherit cursor-auto; + } + } + } + } + + &.ghost { + div.control > div.input-container { + & > input { + @apply outline-none; + + &:hover { + @apply outline-none; + } + } + } + } +} + +div.machine-tags-content { + @apply rounded-sm bg-def-1 border border-def-2; + + transform-origin: var(--kb-combobox-content-transform-origin); + animation: machineTagsContentHide 250ms ease-in forwards; + + &[data-expanded] { + animation: machineTagsContentShow 250ms ease-out; + } + + & > ul.listbox { + overflow-y: auto; + max-height: 360px; + + @apply px-2 py-3; + + &:focus { + outline: none; + } + + li.item { + @apply flex items-center justify-between; + @apply relative px-2 py-1; + @apply select-none outline-none rounded-[0.25rem]; + + color: hsl(240 4% 16%); + height: 32px; + + &[data-disabled] { + color: hsl(240 5% 65%); + opacity: 0.5; + pointer-events: none; + } + + &[data-highlighted] { + @apply outline-none bg-def-4; + } + } + + .item-indicator { + height: 20px; + width: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + } + } +} + +div.machine-tags-control { + @apply flex flex-col w-full gap-2; + + & > div.selected-options { + @apply flex gap-2 flex-wrap w-full; + } + + & > div.input-container { + @apply w-full flex gap-2; + } +} + +@keyframes machineTagsContentShow { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes machineTagsContentHide { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-8px); + } +} diff --git a/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx b/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx new file mode 100644 index 000000000..8eada9313 --- /dev/null +++ b/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx @@ -0,0 +1,164 @@ +import { + Combobox as KCombobox, + Combobox, + ComboboxRootProps, +} from "@kobalte/core/combobox"; +import { FieldProps } from "./Field"; +import { Accessor, createSignal, For, Show } from "solid-js"; +import Icon from "../Icon/Icon"; +import cx from "classnames"; +import { Typography } from "@/src/components/Typography/Typography"; +import { + ComboboxItem, + ComboboxItemIndicator, + ComboboxItemLabel, + ComboboxProps, +} from "@/src/components/Form/Combobox"; +import { a as CollectionNode } from "@kobalte/core/dist/types-f8ae18e5"; +import { Tag } from "@/src/components/Tag/Tag"; + +import "./MachineTags.css"; +import { Label } from "@/src/components/Form/Label"; +import { Orienter } from "@/src/components/Form/Orienter"; + +interface MachineTag { + value: string; + disabled?: boolean; +} + +export type MachineTagsProps = FieldProps & ComboboxRootProps & {}; + +const deduplicateOptions = (options: MachineTag[]) => { + return options.filter((option, index) => { + return options.findIndex((o) => o.value === option.value) === index; + }); +}; + +const sortOptions = (options: MachineTag[]) => { + return options.sort((a, b) => { + if (a.disabled && !b.disabled) return -1; + return a.value.localeCompare(b.value); + }); +}; + +const ItemComponent = (props: { item: CollectionNode }) => { + return ( + + + + {props.item.textValue} + + + + + + + ); +}; + +const Control = (props: { + inverted?: boolean; + readOnly?: boolean; + disabled?: boolean; +}) => ( + class="control"> + {(state) => ( +
+ + {(option) => ( + state.remove(option), + } + } + /> + )} + + +
+ + + + + + +
+
+
+ )} + +); + +export const MachineTags = (props: MachineTagsProps) => { + // options which are applied to all machines and cannot be interacted with + const alwaysOptions = [{ value: "All", disabled: true }]; + + const [options, setOptions] = createSignal( + sortOptions([ + ...alwaysOptions, + ...((props.defaultValue as MachineTag[]) || []), + ]), + ); + + const _setOptions = (options: MachineTag[] | null) => { + if (options === null) setOptions([]); + setOptions( + sortOptions(deduplicateOptions([...alwaysOptions, ...(options || [])])), + ); + }; + + const align = () => { + if (props.readOnly) { + return "center"; + } else { + return props.orientation === "horizontal" ? "start" : "center"; + } + }; + + return ( + + multiple + class={cx("form-field", "machine-tags", props.size, props.orientation, { + inverted: props.inverted, + ghost: props.ghost, + })} + {...props} + itemComponent={ItemComponent} + defaultValue={options()} + value={options()} + optionLabel={(option) => option.value} + optionTextValue="value" + options={options()} + onChange={_setOptions} + placeholder="Tag a machine" + > + + + + + + + + + + ); +}; diff --git a/pkgs/clan-app/ui/src/components/Sidebar/SidebarPane.stories.tsx b/pkgs/clan-app/ui/src/components/Sidebar/SidebarPane.stories.tsx index 7121c9685..e7ffff007 100644 --- a/pkgs/clan-app/ui/src/components/Sidebar/SidebarPane.stories.tsx +++ b/pkgs/clan-app/ui/src/components/Sidebar/SidebarPane.stories.tsx @@ -12,6 +12,8 @@ import { SidebarSectionForm } from "@/src/components/Sidebar/SidebarSectionForm" import * as v from "valibot"; import { splitProps } from "solid-js"; import { Typography } from "@/src/components/Typography/Typography"; +import { Combobox } from "../Form/Combobox"; +import { MachineTags } from "@/src/components/Form/MachineTags"; type Story = StoryObj; @@ -126,32 +128,29 @@ export const Default: Story = { )} {/* todo fix tags component */} - {/* {*/} - {/* console.log("saving general");*/} - {/* }}*/} - {/*>*/} - {/* {({ editing, Field }) => (*/} - {/* */} - {/* {(field, input) => (*/} - {/* */} - {/* )}*/} - {/* */} - {/* )}*/} - {/**/} + { + console.log("saving general"); + }} + > + {({ editing, Field }) => ( + + {(field, input) => ( + + )} + + )} + Static Content