Skip to content

Commit

Permalink
feat: Generic clickable controls w/ mode=tap #77
Browse files Browse the repository at this point in the history
  • Loading branch information
lo5 committed Aug 26, 2022
1 parent 55296fc commit 254ec83
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 207 deletions.
2 changes: 1 addition & 1 deletion help/docs/guide/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ view(f'You chose {choice}.')

## More examples

All boxes support `hint=` and `help=`, except containers like rows, columns, or tabs.
Almost all boxes in Nitro support `hint=` and `help=`.


```py
Expand Down
2 changes: 1 addition & 1 deletion py/pkg/docs/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def hint_localization(view: View): # height 2


# ## More examples
# All boxes support `hint=` and `help=`, except containers like rows, columns, or tabs.
# Almost all boxes in Nitro support `hint=` and `help=`.
def help_examples(view: View): # height 10
hint = 'Here is a hint about this box!'
flavors = ['Vanilla', 'Strawberry', 'Blueberry']
Expand Down
6 changes: 3 additions & 3 deletions web/src/body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { signal, xid } from './core';
import { hasActions } from './heuristics';
import { Box } from './protocol';
import { make } from './ui';
import { Zone } from './zone';
import { XBox } from './box';

const
continueButton: Box = {
Expand All @@ -33,7 +33,7 @@ export const Body = ({ client }: { client: Client }) => {
const boxes: Box[] = makeContinuable(client.body)
return (
<div className='main'>
<Zone context={client.context} box={{ xid: 'main', index: -1, modes: new Set(['col']), items: boxes, options: [] }} />
<XBox context={client.context} box={{ xid: 'main', index: -1, modes: new Set(['col']), items: boxes, options: [] }} />
</div>
)
}
Expand Down Expand Up @@ -67,7 +67,7 @@ export const Popup = make(({ client }: { client: Client }) => {
modalProps={modalProps}
hidden={hidden}
>
<Zone context={client.context} box={{ xid: 'popup', index: -1, modes: new Set(['col']), items: boxes, options: [] }} />
<XBox context={client.context} box={{ xid: 'popup', index: -1, modes: new Set(['col']), items: boxes, options: [] }} />
</Dialog >
)
}
Expand Down
150 changes: 137 additions & 13 deletions web/src/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { INavLink, INavLinkGroup, Nav, Pivot, PivotItem } from '@fluentui/react';
import React from 'react';
import { Banner } from './banner';
import { Buttons } from './buttons';
Expand All @@ -23,11 +24,14 @@ import { jump } from './client';
import { ColorPalette } from './color_palette';
import { ColorPicker } from './color_picker';
import { ComboBox } from './combobox';
import { B, by, on, signal, Signal, zip } from './core';
import { css } from './css';
import { DatePicker } from './date_picker';
import { Dropdown } from './dropdown';
import { Droplist } from './droplist';
import { Expander } from './expander';
import { FileUpload } from './file_upload';
import { Help } from './help';
import { PluginBox } from './plugin';
import { ProgressBar } from './progress';
import { Rating } from './rating';
Expand All @@ -42,14 +46,68 @@ import { Textbox } from './textbox';
import { TextBlock } from './text_block';
import { TimePicker } from './time_picker';
import { Toggle } from './toggle';
import { BoxProps } from './ui';
import { BoxProps, make } from './ui';
import { WebView } from './webview';

export const XBox = ({ context, box }: BoxProps) => { // recursive

export const XBox = ({ context: root, box }: BoxProps) => { // recursive
const
{ modes, options } = box,
editable = modes.has('editable'),
multiple = modes.has('multi')
{ modes, options, items, path } = box,
context = box.index >= 0 ? root.scoped(box.index, box.pid ?? box.xid) : root,
onClick = modes.has('tap')
? () => {
const v = box.value ?? box.name ?? box.text
if (v) {
context.record(v as any)
context.commit()
}
}
: path
? (e: React.MouseEvent<HTMLDivElement>) => {
jump(path ?? '')
e.preventDefault()
}
: undefined,
pointer = onClick ? 'cursor-pointer' : undefined

if (items) {
if (options?.length) {
return modes.has('col')
? <NavSet context={context} box={box} />
: <TabSet context={context} box={box} />
}

const children = items.map(box => (
<Help key={box.xid} context={context} box={box}>
<XBox context={context} box={box} />
</Help>
))

if (modes.has('group')) return <Expander box={box}>{children}</Expander>

const
background = box.image ? { backgroundImage: `url(${box.image})` } : undefined,
flex = modes.has('col') ? 'flex flex-col gap-2' : modes.has('row') ? 'flex gap-2' : undefined

return (
<div
className={css(flex, pointer, box.style)}
data-name={box.name ?? undefined}
onClick={onClick}
style={background}
>{children}</div>
)
} // !items

if (box.image) return (
<img
className={css(box.style)}
data-name={box.name ?? undefined}
alt={box.text}
src={box.image}
/>
)

if (modes.has('md')) {
return <TextBlock context={context} box={box} />
} else if (modes.has('button')) {
Expand All @@ -71,9 +129,9 @@ export const XBox = ({ context, box }: BoxProps) => { // recursive
} else if (modes.has('file')) {
return <FileUpload context={context} box={box} />
} else if (modes.has('menu')) {
return editable
return modes.has('editable')
? <ComboBox context={context} box={box} />
: multiple
: modes.has('multi')
? <Droplist context={context} box={box} />
: <Dropdown context={context} box={box} />
} else if (modes.has('number')) {
Expand Down Expand Up @@ -114,10 +172,76 @@ export const XBox = ({ context, box }: BoxProps) => { // recursive
}
}

const onClick = box.path ? (e: React.MouseEvent<HTMLDivElement>) => {
jump(box.path ?? '')
e.preventDefault()
} : undefined
const className = box.path ? css('cursor-pointer', box.style) : css(box.style)
return <div className={className} onClick={onClick}>{box.text ?? ''}</div>
return <div className={css(pointer, box.style)} onClick={onClick}>{box.text ?? ''}</div>
}

const NavSetItem = make(({ visibleB, children }: { visibleB: Signal<B>, children: React.ReactNode }) => {
const
render = () => {
const style: React.CSSProperties = { display: visibleB() ? 'block' : 'none' }
return <div style={style}>{children}</div>
}
return { render, visibleB }
})

const NavSet = make(({ context, box }: BoxProps) => {
const
{ style, items: rawItems, options: rawOptions } = box,
items = rawItems ?? [],
options = rawOptions ?? [],
selectedIndex = items.findIndex(box => box.modes.has('open')),
selectedIndexB = signal(selectedIndex < 0 ? 0 : selectedIndex),
selectedKeyB = by(selectedIndexB, i => String(options[i].value)),
visibleBs = options.map((_, i) => signal(i === selectedIndexB() ? true : false)),
tabs = zip(items, visibleBs, (box, visibleB) => (
<NavSetItem key={box.xid} visibleB={visibleB}>
<XBox context={context} box={box} />
</NavSetItem>
)),
links: INavLink[] = zip(items, options, (box, o) => ({
key: String(o.value),
name: o.text ?? '',
url: '',
icon: box.icon,
})),
groups: INavLinkGroup[] = [{ links }],
onLinkClick = (ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => {
if (item) selectedIndexB(links.indexOf(item))
},
render = () => (
<div className={css('flex gap-4', style)}>
<div className={css('border-r')}>
<Nav groups={groups} onLinkClick={onLinkClick} selectedKey={selectedKeyB()} />
</div>
<div className={css('grow')}>{tabs}</div>
</div>
)

on(selectedIndexB, k => visibleBs.forEach((v, i) => v(i === k ? true : false)))

return { render, selectedKeyB }
})

const TabSet = ({ context, box }: BoxProps) => {
const
{ style, items: rawItems, options: rawOptions } = box,
items = rawItems ?? [],
options = rawOptions ?? [],
tabs = zip(options, items, (opt, box, i) => (
<PivotItem
key={box.xid}
headerText={opt.text ?? `Tab ${i + 1}`}
itemKey={box.xid} itemIcon={box.icon ?? undefined}
style={{ padding: '8px 0' }}
>
<XBox context={context} box={box} />
</PivotItem>
)),
selectedKey = (items.find(box => box.modes.has('open')) ?? items[0]).xid

return (
<div className={css(style)} data-name={box.name ?? undefined} >
<Pivot defaultSelectedKey={selectedKey}>{tabs}</Pivot>
</div>
)
}
6 changes: 3 additions & 3 deletions web/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import hotkeys from "hotkeys-js";
import { B, Dict, isS, newIncr, on, S, Signal, signal, U, V } from './core';
import { reIndex, sanitizeBox, sanitizeOptions } from './heuristics';
import { freeze, sanitizeBox, sanitizeOptions } from './heuristics';
import { installPlugins } from './plugin';
import { Box, DisplayMode, Edit, EditType, Input, InputValue, Message, MessageType, Option, Server, ServerEvent, ServerEventT, Theme } from './protocol';
import { applyTheme } from './theme';
Expand Down Expand Up @@ -261,7 +261,7 @@ export const newClient = (server: Server) => {
if (box.popup) {
popup.length = 0
popup.push(box)
reIndex(popup, newIncr())
freeze(popup)
} else {
popup.length = 0 // clear any existing popup

Expand Down Expand Up @@ -342,7 +342,7 @@ export const newClient = (server: Server) => {
}
}
}
reIndex(body, newIncr())
freeze(body)
}
busyB(false)
}
Expand Down
50 changes: 29 additions & 21 deletions web/src/help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import { IButtonProps, IconButton, Label, Panel, Stack, TeachingBubble } from '@fluentui/react';
import React from 'react';
import { B, on, S, Signal, signal, splitLines, xid } from './core';
import { labeledModes } from './heuristics';
import { markdown } from './markdown';
import { Context, make } from './ui';
import { Box } from './protocol';
import { BoxProps, Context, make } from './ui';

const tokenize = (hint?: S) => {
if (!hint) return []
Expand Down Expand Up @@ -69,29 +71,35 @@ const Hint = make(({ context, hint: rawHint, help }: { context: Context, hint?:
return { render, visibleB }
})

export const Help = make(({ context, hint, help, offset, children }: { context: Context, hint?: S, help?: S, offset: B, children: JSX.Element }) => {
const
render = () => {
const
button = <Hint context={context} hint={hint} help={help} />,
maybeWithLabel = offset
? (
<Stack>
<Stack.Item><Label>&nbsp;</Label></Stack.Item>
<Stack.Item>{button}</Stack.Item>
</Stack>
)
: button
const hasLabel = (box: Box): B => {
const { modes } = box
for (const t of labeledModes) if (modes.has(t) && box.text) return true
return false
}

return (
<Stack horizontal tokens={{ childrenGap: 5 }}>
<Stack.Item grow>{children}</Stack.Item>
<Stack.Item>{maybeWithLabel}</Stack.Item>
export const Help = ({ context, box, children }: BoxProps & { children: JSX.Element }) => {
const { help, hint } = box

if (!(hint || help)) return children

const
button = <Hint context={context} hint={hint} help={help} />,
maybeWithLabel = hasLabel(box)
? (
<Stack>
<Stack.Item><Label>&nbsp;</Label></Stack.Item>
<Stack.Item>{button}</Stack.Item>
</Stack>
)
}
return { render }
})
: button

return (
<Stack horizontal tokens={{ childrenGap: 5 }}>
<Stack.Item grow>{children}</Stack.Item>
<Stack.Item>{maybeWithLabel}</Stack.Item>
</Stack>
)
}

const Doc = make(({ html, helpE }: { html: S, helpE: Signal<S> }) => {
const
Expand Down
Loading

0 comments on commit 254ec83

Please sign in to comment.