import { isColorLike, toColor, toColorString } from '@o/color'
import { fromEntries, idFn, ImmutableUpdateFn, isDefined, selectDefined } from '@o/utils'
import { configureCSS, configureGloss, GlossDefaultConfig } from 'gloss/config'
import sum from 'hash-sum'
import { Context, createContext, FunctionComponent, isValidElement, useState } from 'react'
import { ListItemProps, ListItemViewProps } from '../lists/ListItemViewProps'
import { SimpleTextProps } from '../text/SimpleText'
import { TitleProps } from '../text/Title'
import { Bit } from './BitLike'
/**
* WARNING: dont import gloss directly here, only gloss/config!
*/
// TODO duplicate of kit definition
type PersistedStateOptions = {
persist?: 'off'
}
export type ConfigureUIProps = {
mediaQueries: { [key: string]: string }
// configure a custom icon for all surfaces
// this could be made generic for any part of the ui kit to override
useIcon: any
getIconForBit: (bit: Bit) => React.ReactNode
// set a custom item key getter for lists/tables
getItemKey: (item: any, index: number) => string | number
handleLink: (event: Event, url: string) => any
// set a custom persistence function for appState
useAppState: (
id: string | false,
defaultState: A,
options?: PersistedStateOptions,
) => [A, ImmutableUpdateFn]
// set a custom persistence function for userState
useUserState: (
id: string | false,
defaultState: A,
options?: PersistedStateOptions,
) => [A, ImmutableUpdateFn]
// you can control how list items and bits render
customItems: {
// maps Bit.contentType to a custom view
[key: string]: CustomItemDescription
}
// (INTERNAL) use your own store context
StoreContext: Context
// helpful for custom fonts, etc
defaultProps: {
title?: Partial | null
text?: Partial | null
}
loadBit?: (id: number) => Promise
}
export type CustomItemView = FunctionComponent
export type CustomItemDescription = {
listItem?: CustomItemView
item?: CustomItemView
getItemProps?: (item: Bit) => ListItemProps
}
// safe for react components
const hash = x =>
sum(
fromEntries(
Object.entries(x).map(x => (isValidElement(x[1] as any) ? [x[0], (x[1] as any).key] : x)),
),
)
export let Config: ConfigureUIProps = {
useIcon: null,
getIconForBit: bit => bit.appIdentifier,
defaultProps: {},
customItems: {},
mediaQueries: GlossDefaultConfig.mediaQueries,
// used to configure how the UI persists non-temporal state
useUserState: useState,
useAppState: useState,
handleLink: idFn,
StoreContext: createContext(null),
getItemKey: (x, index) => {
if (!x) {
console.warn('NO ITEM', x)
return selectDefined(index, `${Math.random()}`)
}
const item = x.item || x
const key = item.id || item.key || item.identifier
if (isDefined(key)) {
return `${key}`
}
if (typeof index === 'undefined') {
return `${hash(x)}`
}
return index
},
}
// allow multiple configurations because mediaQueries is sensitive to start order
// so its too hard to thread things around without being able to run multiple
export function configureUI(opts: Partial) {
Object.assign(Config, opts)
configureCSS({
isColor: isColorLike,
toColor: toColorString,
})
configureGloss({
toColor,
mediaQueries: opts.mediaQueries || Config.mediaQueries,
})
}
if (typeof module !== 'undefined' && module.hot) {
module.hot.accept(() => {
if (module.hot) {
Config = module.hot.data || Config
}
})
module.hot.dispose(_ => {
_.data = Config
})
}