UNPKG

@reusable-ui/groupable

Version:

Groups a list of UIs into a single UI.

320 lines (314 loc) 16.6 kB
// cssfn: import { // writes css in javascript: rule, children, nextSiblings, style, vars, iif, cssVars, // writes complex stylesheets in simpler way: memoizeStyle, } from '@cssfn/core'; // writes css in javascript // reusable-ui features: import { // hooks: usesBorder, } from '@reusable-ui/border'; // border (stroke) stuff of UI import { // hooks: usesPadding, } from '@reusable-ui/padding'; // padding (inner spacing) stuff of UI // reusable-ui variants: import { defaultBlockOrientationableOptions, usesOrientationable, } from '@reusable-ui/orientationable'; // a capability of UI to rotate its layout // defaults: export const defaultOrientationableOptions = defaultBlockOrientationableOptions; const [groupableVars] = cssVars({ prefix: 'gr', minify: false }); // shared variables: ensures the server-side & client-side have the same generated css variable names // rules: // :where(...) => zero specificity => easy to overwrite further: export const ifVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign))', styles); export const ifFirstVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign)):where(:first-child, .first-visible-child)', styles); export const ifLastVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign)):where( :last-child, .last-visible-child)', styles); export const ifNotFirstVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign, :first-child, .first-visible-child))', styles); export const ifNotLastVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign, :last-child, .last-visible-child))', styles); export const ifSecondVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign)):where(:nth-child(2))', styles); export const ifNotSecondVisibleChild = (styles) => rule(':not(:where(.overlay, .foreign, :nth-child(2)))', styles); const groupableChildrenRuleOfOrientationInline = memoizeStyle(() => { // dependencies: // features: const { borderVars } = usesBorder(); return style({ ...ifFirstVisibleChild({ ...vars({ /* if the_current_element is a_child_of_container and also a_separator, the deleted `groupableVars.borderWidth` in separator must be pointed to container, so we can calculate the correct inner_borderRadius. that's why we set `!important` to the `groupableVars.borderWidth`. */ [groupableVars.borderWidth]: ['inherit', '!important'], /* the two props below using `!important` to overwrite `groupableRule()`, if the_current_element is a_child_of_container and also a_separator. */ [groupableVars.borderStartStartRadius]: ['inherit', '!important'], [groupableVars.borderEndStartRadius]: ['inherit', '!important'], // reads parent's prop }), ...style({ // borders: // add rounded corners on left: /* the two props below using `!important` to overwrite `borderRule()`, if the_current_element defines `...borderRule()`. */ [borderVars.borderStartStartRadius]: [`calc(${groupableVars.borderStartStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], [borderVars.borderEndStartRadius]: [`calc(${groupableVars.borderEndStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], /* recursive calculation of borderRadius is not supported yet */ }), }), ...ifLastVisibleChild({ ...vars({ /* if the_current_element is a_child_of_container and also a_separator, the deleted `groupableVars.borderWidth` in separator must be pointed to container, so we can calculate the correct inner_borderRadius. that's why we set `!important` to the `groupableVars.borderWidth`. */ [groupableVars.borderWidth]: ['inherit', '!important'], /* the two props below using `!important` to overwrite `groupableRule()`, if the_current_element is a_child_of_container and also a_separator. */ [groupableVars.borderStartEndRadius]: ['inherit', '!important'], [groupableVars.borderEndEndRadius]: ['inherit', '!important'], // reads parent's prop }), ...style({ // borders: // add rounded corners on right: /* the two props below using `!important` to overwrite `borderRule()`, if the_current_element defines `...borderRule()`. */ [borderVars.borderStartEndRadius]: [`calc(${groupableVars.borderStartEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], [borderVars.borderEndEndRadius]: [`calc(${groupableVars.borderEndEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], /* recursive calculation of borderRadius is not supported yet */ }), }), }); }); const groupableChildrenRuleOfOrientationBlock = memoizeStyle(() => { // dependencies: // features: const { borderVars } = usesBorder(); return style({ ...ifFirstVisibleChild({ ...vars({ /* if the_current_element is a_child_of_container and also a_separator, the deleted `groupableVars.borderWidth` in separator must be pointed to container, so we can calculate the correct inner_borderRadius. that's why we set `!important` to the `groupableVars.borderWidth`. */ [groupableVars.borderWidth]: ['inherit', '!important'], /* the two props below using `!important` to overwrite `groupableRule()`, if the_current_element is a_child_of_container and also a_separator. */ [groupableVars.borderStartStartRadius]: ['inherit', '!important'], [groupableVars.borderStartEndRadius]: ['inherit', '!important'], // reads parent's prop }), ...style({ // borders: // add rounded corners on top: /* the two props below using `!important` to overwrite `borderRule()`, if the_current_element defines `...borderRule()`. */ [borderVars.borderStartStartRadius]: [`calc(${groupableVars.borderStartStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], [borderVars.borderStartEndRadius]: [`calc(${groupableVars.borderStartEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], /* recursive calculation of borderRadius is not supported yet */ }), }), ...ifLastVisibleChild({ ...vars({ /* if the_current_element is a_child_of_container and also a_separator, the deleted `groupableVars.borderWidth` in separator must be pointed to container, so we can calculate the correct inner_borderRadius. that's why we set `!important` to the `groupableVars.borderWidth`. */ [groupableVars.borderWidth]: ['inherit', '!important'], /* the two props below using `!important` to overwrite `groupableRule()`, if the_current_element is a_child_of_container and also a_separator. */ [groupableVars.borderEndStartRadius]: ['inherit', '!important'], [groupableVars.borderEndEndRadius]: ['inherit', '!important'], // reads parent's prop }), ...style({ // borders: // add rounded corners on bottom: /* the two props below using `!important` to overwrite `borderRule()`, if the_current_element defines `...borderRule()`. */ [borderVars.borderEndStartRadius]: [`calc(${groupableVars.borderEndStartRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], [borderVars.borderEndEndRadius]: [`calc(${groupableVars.borderEndEndRadius} - ${groupableVars.borderWidth} - min(${groupableVars.borderWidth}, 0.5px))`, '!important'], /* recursive calculation of borderRadius is not supported yet */ }), }), }); }); /** * Groups a list of UIs into a single UI. * @param options Options of `groupableRule`. * @returns A `GroupableStuff` represents the group rules. */ export const usesGroupable = (options) => { // options: const orientationableStuff = usesOrientationable(options, defaultOrientationableOptions); const { ifOrientationInline, ifOrientationBlock } = orientationableStuff; options = orientationableStuff; const { itemsSelector = '*', } = options; // dependencies: // features: const { borderVars } = usesBorder(); const { paddingVars } = usesPadding(); return { groupableRule: () => style({ ...vars({ // borders: [groupableVars.borderWidth]: borderVars.borderWidth, [groupableVars.borderStartStartRadius]: borderVars.borderStartStartRadius, [groupableVars.borderStartEndRadius]: borderVars.borderStartEndRadius, [groupableVars.borderEndStartRadius]: borderVars.borderEndStartRadius, [groupableVars.borderEndEndRadius]: borderVars.borderEndEndRadius, // spacings: [groupableVars.paddingInline]: paddingVars.paddingInline, [groupableVars.paddingBlock]: paddingVars.paddingBlock, }), // ...style({ // // borders: // overflow : 'hidden', // clip the children at the rounded corners // bad idea, causing child's focus boxShadow to be clipped off // }), ...ifOrientationInline(() => // inline children(itemsSelector, groupableChildrenRuleOfOrientationInline)), ...ifOrientationBlock(() => // block children(itemsSelector, groupableChildrenRuleOfOrientationBlock)), }), separatorRule: () => style({ ...ifOrientationInline(() => // inline ((options === undefined) || ((options.itemsSelector === '*') && (options.swapFirstItem === false))) ? getDefaultSeparatorRuleInline() : createSeparatorRuleInline(options)), ...ifOrientationBlock(() => // block ((options === undefined) || ((options.itemsSelector === '*') && (options.swapFirstItem === false))) ? getDefaultSeparatorRuleBlock() : createSeparatorRuleBlock(options)), }), groupableVars, }; }; const createSeparatorRuleOf = (block, options) => { // options: const { itemsSelector = '*', swapFirstItem = false, } = options ?? {}; // dependencies: // features: const { borderVars } = usesBorder(); return style({ ...vars({ /* if the_current_element is a container, the `groupableVars.borderWidth` will be deleted (not follow `borderWidth` again), because the_current_element becomes a separator. use `0px` instead of 0, because the value will be calculated in `calc()` expression. */ [groupableVars.borderWidth]: '0px', // remove border }), ...style({ // borders: boxShadow: null, // remove shadow }), // removes unecessary border stroke: ...style({ // borders: [`border${block ? 'Inline' : 'Block'}Width`]: 0, // remove top-border at the first-child, so that it wouldn't collide with the container's top-border ...ifFirstVisibleChild({ // borders: [`border${block ? 'Block' : 'Inline'}StartWidth`]: 0, // remove top-border }), // remove bottom-border at the last-child, so that it wouldn't collide with the container's bottom-border // *note : *special case* => move the first separator to the second child ...ifLastVisibleChild({ // borders: [`border${block ? 'Block' : 'Inline'}EndWidth`]: 0, // remove bottom-border }), ...iif(swapFirstItem, ifFirstVisibleChild({ // borders: [`border${block ? 'Block' : 'Inline'}EndWidth`]: 0, // remove bottom-border })), // children: // remove double border by removing top-border at the subsequent sibling(s) ...nextSiblings(itemsSelector, { ...ifVisibleChild({ ...iif(!swapFirstItem, style({ // borders: [`border${block ? 'Block' : 'Inline'}StartWidth`]: 0, // remove top-border })), ...iif(swapFirstItem, ifNotSecondVisibleChild({ // borders: [`border${block ? 'Block' : 'Inline'}StartWidth`]: 0, // remove top-border })), }), }), }), // removes unecessary border radius: // although the border stroke was/not removed, it *affects* the children's border radius // do not remove border radius at the parent's corners (:first-child & :last-child) ...ifNotFirstVisibleChild({ ...(block ? style({ // borders: // remove rounded corners on top: [borderVars.borderStartStartRadius]: '0px', [borderVars.borderStartEndRadius]: '0px', }) : style({ // borders: // remove rounded corners on left: [borderVars.borderStartStartRadius]: '0px', [borderVars.borderEndStartRadius]: '0px', })), }), ...ifNotLastVisibleChild({ ...(block ? style({ // borders: // remove rounded corners on bottom: [borderVars.borderEndStartRadius]: '0px', [borderVars.borderEndEndRadius]: '0px', }) : style({ // borders: // remove rounded corners on right: [borderVars.borderStartEndRadius]: '0px', [borderVars.borderEndEndRadius]: '0px', })), }), }); }; const createSeparatorRuleInline = (options) => createSeparatorRuleOf(false, options); const createSeparatorRuleBlock = (options) => createSeparatorRuleOf(true, options); const getDefaultSeparatorRuleInline = memoizeStyle(() => createSeparatorRuleInline()); const getDefaultSeparatorRuleBlock = memoizeStyle(() => createSeparatorRuleBlock()); //#endregion groupable