diff --git a/-tmp/-wip/Image.Svg/Svg.ts b/-tmp/-wip/Image.Svg/Svg.ts new file mode 100644 index 0000000000..4119d19a18 --- /dev/null +++ b/-tmp/-wip/Image.Svg/Svg.ts @@ -0,0 +1,14 @@ +import { SvgRef } from './SvgRef'; +import { SvgElement } from './libs'; + +/** + * Helpers for working with SVG objects within the given container element. + * + * NOTE: + * This helper assumes <svg> data assembled via the webpack plugin + * (see [cell.compiler]). + */ +export const Svg = { + ref: SvgRef, + Element: SvgElement, +}; diff --git a/-tmp/-wip/Image.Svg/SvgRef.ts b/-tmp/-wip/Image.Svg/SvgRef.ts new file mode 100644 index 0000000000..feb5b67c0a --- /dev/null +++ b/-tmp/-wip/Image.Svg/SvgRef.ts @@ -0,0 +1,35 @@ +import * as t from './types'; +import { SVG } from './libs'; + +/** + * A helper for working with <SVG> objects within a given DOM container element. + * + * NOTE: + * This helper assumes <svg> data assembled via the webpack plugin + * (see [cell.compiler]). + */ +export function SvgRef(filename: string, elContainer: HTMLElement): t.SvgRef { + filename = (filename ?? '') + .trim() + .replace(/^\#/, '') + .replace(/\.svg$/, ''); + filename = `${filename}.svg`; + + return { + filename, + find(id: string) { + id = (id || '').trim().replace(/^\#/, ''); + const query = `#${Format.toId(filename)}__${Format.toId(id)}`; + const el = elContainer.querySelector(query); + return el ? SVG(el) : undefined; + }, + }; +} + +/** + * [Helpers] + */ + +const Format = { + toId: (value: string) => value.replace(/\./g, '_'), +}; diff --git a/-tmp/-wip/Image.Svg/dev/DEV.tsx b/-tmp/-wip/Image.Svg/dev/DEV.tsx new file mode 100644 index 0000000000..c4096fee40 --- /dev/null +++ b/-tmp/-wip/Image.Svg/dev/DEV.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { DevActions } from 'sys.ui.dev'; + +import { Sample, SampleProps } from './Sample'; + +type Ctx = { props: SampleProps }; + +/** + * Actions + */ +export const actions = DevActions<Ctx>() + .namespace('ui.Image.Svg') + .context((e) => { + if (e.prev) return e.prev; + return { + props: { width: 600, color: 'dark' }, + }; + }) + + .items((e) => { + e.title('<Svg>'); + + e.button('width: 600', (e) => (e.ctx.props.width = 600)); + e.button('width: 200', (e) => (e.ctx.props.width = 200)); + + e.hr(1, 0.1); + + e.button('color: dark', (e) => (e.ctx.props.color = 'dark')); + e.button('color: blue', (e) => (e.ctx.props.color = 'blue')); + + e.hr(); + }) + + .subject((e) => { + e.settings({ + host: { background: -0.04 }, + layout: { cropmarks: -0.2 }, + }); + + e.render(<Sample {...e.ctx.props} />); + }); + +export default actions; diff --git a/-tmp/-wip/Image.Svg/dev/Sample.tsx b/-tmp/-wip/Image.Svg/dev/Sample.tsx new file mode 100644 index 0000000000..39ba27d054 --- /dev/null +++ b/-tmp/-wip/Image.Svg/dev/Sample.tsx @@ -0,0 +1,35 @@ +import '../types.declare'; + +import React, { useEffect } from 'react'; + +import { Svg } from '..'; +import { COLORS, css } from '../../../common'; +import Image from '../../../../static/images/sample/svg.sample.svg'; + +export type SampleProps = { color: 'dark' | 'blue'; width: number }; + +export const Sample: React.FC<SampleProps> = (props) => { + const { width } = props; + const ref = React.useRef<HTMLDivElement>(null); + + useEffect(() => { + const svg = Svg.ref('svg.sample', ref.current as HTMLElement); + + const isDark = props.color === 'dark'; + const color = isDark ? COLORS.DARK : COLORS.BLUE; + + const tick = svg.find('tick'); + const outline = svg.find('border-outline'); + + tick?.opacity(isDark ? 1 : 0.2); + outline?.stroke(color); + }, [props.color]); + + const styles = { base: css({}) }; + + return ( + <div ref={ref} {...css(styles.base)}> + <Image width={width} /> + </div> + ); +}; diff --git a/-tmp/-wip/Image.Svg/index.ts b/-tmp/-wip/Image.Svg/index.ts new file mode 100644 index 0000000000..6d98b36350 --- /dev/null +++ b/-tmp/-wip/Image.Svg/index.ts @@ -0,0 +1,3 @@ +export { Svg } from './Svg'; +export { SvgRef } from './SvgRef'; +export { SvgElement } from './libs'; diff --git a/-tmp/-wip/Image.Svg/libs.ts b/-tmp/-wip/Image.Svg/libs.ts new file mode 100644 index 0000000000..27e69bb61d --- /dev/null +++ b/-tmp/-wip/Image.Svg/libs.ts @@ -0,0 +1 @@ +export { SVG, Element as SvgElement } from '@svgdotjs/svg.js'; diff --git a/-tmp/-wip/Image.Svg/types.declare.ts b/-tmp/-wip/Image.Svg/types.declare.ts new file mode 100644 index 0000000000..499de023b4 --- /dev/null +++ b/-tmp/-wip/Image.Svg/types.declare.ts @@ -0,0 +1,11 @@ +/** + * Importing <SVG> assets. + * See: + * https://react-svgr.com + */ +declare module '*.svg' { + import React = require('react'); + type Svg = React.SVGProps<SVGSVGElement> & { width?: number; height?: number }; + export const ReactComponent: React.FC<Svg>; + export default ReactComponent; +} diff --git a/-tmp/-wip/Image.Svg/types.ts b/-tmp/-wip/Image.Svg/types.ts new file mode 100644 index 0000000000..d69b201621 --- /dev/null +++ b/-tmp/-wip/Image.Svg/types.ts @@ -0,0 +1,6 @@ +import { Element as SvgElement } from '@svgdotjs/svg.js'; + +export type SvgRef = { + filename: string; + find(id: string): SvgElement | undefined; +}; diff --git a/.github/workflows/Video.tsx b/.github/workflows/Video.tsx deleted file mode 100644 index 8d465ab698..0000000000 --- a/.github/workflows/Video.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// @ts-types="@types/react" -import React from 'react'; - -// import '@vidstack/react/player/styles/base.css'; -// import '@vidstack/react/player/styles/plyr/theme.css'; - -import { MediaPlayer, MediaProvider } from '@vidstack/react'; -// import { PlyrLayout, plyrLayoutIcons } from '@vidstack/react/player/layouts/plyr'; - -console.log('MediaPlayer', MediaPlayer); - -/** - * TODO š· - add workspace/plugin refs for VitePress ā see: @sys/driver-vite - */ -import { Foo } from '@sys/tmp/ui'; - -export type VideoProps = { - title?: string; - src?: string; -}; - -/** - * Component - */ -export const Video: React.FC<VideoProps> = (props: VideoProps) => { - const src = props.src || DEFAULTS.src; - - // const elPlayer = ( - // <MediaPlayer title={props.title} src={src} playsInline={true}> - // <MediaProvider /> - // <PlyrLayout - // // thumbnails="https://files.vidstack.io/sprite-fight/thumbnails.vtt" - // icons={plyrLayoutIcons} - // /> - // </MediaPlayer> - // ); - - // TEMP š· - return ( - <div> - <div> - import: <Foo /> - </div> - {/* {elPlayer} */} - </div> - ); -}; - -/** - * Constants - */ -export const DEFAULTS = { - src: 'vimeo/499921561', // Tubes. -} as const; diff --git a/.github/workflows/jsr.yaml b/.github/workflows/jsr.yaml index c5323c288a..c0c292fb44 100644 --- a/.github/workflows/jsr.yaml +++ b/.github/workflows/jsr.yaml @@ -25,10 +25,21 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: denoland/setup-deno@v1 + + - name: 'Install ESM Runtime: Deno 2.x' + uses: denoland/setup-deno@v1 with: deno-version: v2 + - name: 'Install ES Modules from JSR: https://jsr.io/@sys' + run: deno task info + + - name: Deno Info + run: deno info && deno --version + + - name: System Info + run: deno task info + - name: publish module ā "@sys/types" run: | cd code/sys/types @@ -137,6 +148,12 @@ jobs: deno task test --parallel deno publish --allow-dirty + - name: publish module ā "@sys/ui-react-components" + run: | + cd code/sys.ui/ui-react-components + deno task test --parallel + deno publish --allow-dirty + # š· # - name: publish module ā "@sys/driver-automerge" # run: | diff --git a/.github/workflows/jsr.yaml_ b/.github/workflows/jsr.yaml_ index 0333ddb396..41f1e48fd9 100644 --- a/.github/workflows/jsr.yaml_ +++ b/.github/workflows/jsr.yaml_ @@ -34,3 +34,15 @@ jobs: cd code/sys.driver/driver-vite deno task test --parallel --trace-leaks deno publish --allow-dirty + + - name: publish module ā "@sys/driver-vitepress" + run: | + cd code/sys.driver/driver-vitepress + deno task test --parallel --trace-leaks + deno publish --allow-dirty + + - name: publish module ā "@sys/tmp" + run: | + cd code/sys.tmp + deno task test --parallel --trace-leaks + deno publish --allow-dirty diff --git a/README.md b/README.md index 407b7b563f..3d125c0086 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ [jsr-ci-url]: https://github.com/sys-repo/sys/actions/workflows/jsr.yaml - Monorepo comprising the core set of shared `/sys` "system" modules that flexibly compose into varying arrangements of (1) extremely-late-bound, (2) strongly typed, (3) decentralised, "cell like" functional processes. - modules: [sys](/code/sys/) ā standard libs diff --git a/code/-tmpl/-test.ui.ts b/code/-tmpl/-test.ui.ts new file mode 100644 index 0000000000..6ef3bbc75e --- /dev/null +++ b/code/-tmpl/-test.ui.ts @@ -0,0 +1,7 @@ +/** + * @module + * Testing tools running in the browser/ui. + */ +export { expect } from '@sys/std/testing'; +export { Dev, Spec } from '@sys/ui-react-devharness'; +export * from './common.ts'; diff --git a/code/-tmpl/common.t.ts b/code/-tmpl/common.t.ts new file mode 100644 index 0000000000..65fc1d3b01 --- /dev/null +++ b/code/-tmpl/common.t.ts @@ -0,0 +1,10 @@ +/** + * š· + * NB: placeholder type exports to ensure template imports don't error. + */ +export type * from './deno/src/common/t.ts'; +export type * from './m.mod.ui/t.ts'; +export type * from './m.mod/t.ts'; + +export type * from '@sys/types/t'; +export type { CssInput } from '@sys/ui-css/t'; diff --git a/code/-tmpl/common.ts b/code/-tmpl/common.ts index c0fb3991aa..440e91203b 100644 --- a/code/-tmpl/common.ts +++ b/code/-tmpl/common.ts @@ -1,2 +1,13 @@ -// NB: placeholder to ensure template imports don't error. +/** + * š· + * NB: placeholder exports to ensure template imports don't error. + */ +export type * as t from './common.t.ts'; export * from './deno/src/common.ts'; + +/** + * UI Refs: + */ +export { Color, css } from '@sys/ui-css'; +export { Signal } from '@sys/ui-react'; +export { Button } from '@sys/ui-react-components'; diff --git a/code/-tmpl/deno/deno.json b/code/-tmpl/deno/deno.json index 0f09aee165..2a9035f348 100644 --- a/code/-tmpl/deno/deno.json +++ b/code/-tmpl/deno/deno.json @@ -5,6 +5,7 @@ "tasks": { "lint": "deno lint", "dry": "deno publish --allow-dirty --dry-run", + "check": "deno check src/*", "clean": "deno run -RWE ./-scripts/-clean.ts", "test": "deno test -RWNE --allow-run --allow-ffi", "dev": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-dev.ts", diff --git a/code/-tmpl/deno/src/common/libs.ts b/code/-tmpl/deno/src/common/libs.ts index eb7b6f00c7..70c75258ea 100644 --- a/code/-tmpl/deno/src/common/libs.ts +++ b/code/-tmpl/deno/src/common/libs.ts @@ -1 +1 @@ -export { Err, Pkg, Time } from '@sys/std'; +export { Err, Pkg, rx, Signal, Time } from '@sys/std'; diff --git a/code/-tmpl/deno/src/pkg.ts b/code/-tmpl/deno/src/pkg.ts index 79cb3f9c80..68bbe9281e 100644 --- a/code/-tmpl/deno/src/pkg.ts +++ b/code/-tmpl/deno/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/name', version: '0.0.0' }; diff --git a/code/-tmpl/m.mod.ui/-SPEC.Debug.tsx b/code/-tmpl/m.mod.ui/-SPEC.Debug.tsx new file mode 100644 index 0000000000..e9f74789b8 --- /dev/null +++ b/code/-tmpl/m.mod.ui/-SPEC.Debug.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { type t, Button, Color, css, Signal } from './common.ts'; + +type P = t.MyComponentProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'Title'}</div> + <div /> + <div></div> + </div> + + <Button + block + label={() => `theme: ${p.theme.value ?? '<undefined>'}`} + onClick={() => Signal.cycle<P['theme']>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + </div> + ); +}; diff --git a/code/-tmpl/m.mod.ui/-SPEC.tsx b/code/-tmpl/m.mod.ui/-SPEC.tsx new file mode 100644 index 0000000000..5a8a5966c3 --- /dev/null +++ b/code/-tmpl/m.mod.ui/-SPEC.tsx @@ -0,0 +1,28 @@ +import { Dev, Spec, Signal } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { MyComponent } from './mod.ts'; + +export default Spec.describe('MyComponent', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size() + .display('grid') + .render((e) => <MyComponent theme={p.theme.value} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/-tmpl/m.mod.ui/common.ts b/code/-tmpl/m.mod.ui/common.ts new file mode 100644 index 0000000000..a45006948c --- /dev/null +++ b/code/-tmpl/m.mod.ui/common.ts @@ -0,0 +1,7 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; +export const D = DEFAULTS; diff --git a/code/-tmpl/m.mod.ui/mod.ts b/code/-tmpl/m.mod.ui/mod.ts new file mode 100644 index 0000000000..e089acf488 --- /dev/null +++ b/code/-tmpl/m.mod.ui/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { MyComponent } from './ui.tsx'; diff --git a/code/-tmpl/m.mod.ui/t.ts b/code/-tmpl/m.mod.ui/t.ts new file mode 100644 index 0000000000..2b3310c354 --- /dev/null +++ b/code/-tmpl/m.mod.ui/t.ts @@ -0,0 +1,9 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type MyComponentProps = { + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/code/-tmpl/m.mod.ui/ui.tsx b/code/-tmpl/m.mod.ui/ui.tsx new file mode 100644 index 0000000000..d9dd7c5c16 --- /dev/null +++ b/code/-tmpl/m.mod.ui/ui.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { type t, Color, css, D } from './common.ts'; + +export const MyComponent: React.FC<t.MyComponentProps> = (props) => { + const {} = props; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + backgroundColor: 'rgba(255, 0, 0, 0.1)' /* RED */, + color: theme.fg, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div>{'š· Hello MyComponent'}</div> + </div> + ); +}; diff --git a/code/-tmpl/pkg/pkg.ts b/code/-tmpl/pkg/pkg.ts new file mode 100644 index 0000000000..d7df14757d --- /dev/null +++ b/code/-tmpl/pkg/pkg.ts @@ -0,0 +1,16 @@ +import type { Pkg } from '@sys/types'; + +/** + * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). + */ +export const pkg: Pkg = { name: '<NAME>', version: '<VERSION>' }; diff --git a/code/sys.driver/driver-automerge/deno.json b/code/sys.driver/driver-automerge/deno.json index dcd4edf0bb..c9eb0a0ba7 100644 --- a/code/sys.driver/driver-automerge/deno.json +++ b/code/sys.driver/driver-automerge/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-automerge", - "version": "0.0.84", + "version": "0.0.94", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys.driver/driver-automerge/src/-test/ui.sample/App.tsx b/code/sys.driver/driver-automerge/src/-test/ui.sample/App.tsx index c524f7bb57..47a80ecd4a 100644 --- a/code/sys.driver/driver-automerge/src/-test/ui.sample/App.tsx +++ b/code/sys.driver/driver-automerge/src/-test/ui.sample/App.tsx @@ -29,7 +29,7 @@ export const App: React.FC<AppProps> = (props) => { }, []); /** - * Render + * Render: */ const theme = Color.theme(props.theme); const styles = { diff --git a/code/sys.driver/driver-automerge/src/pkg.ts b/code/sys.driver/driver-automerge/src/pkg.ts index 79cb3f9c80..2286978f45 100644 --- a/code/sys.driver/driver-automerge/src/pkg.ts +++ b/code/sys.driver/driver-automerge/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-automerge', version: '0.0.94' }; diff --git a/code/sys.driver/driver-deno/deno.json b/code/sys.driver/driver-deno/deno.json index 75e47eefd1..f4216d2238 100644 --- a/code/sys.driver/driver-deno/deno.json +++ b/code/sys.driver/driver-deno/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-deno", - "version": "0.0.86", + "version": "0.0.97", "license": "MIT", "tasks": { "dev": "deno run -RNE --watch ./-scripts/-dev.ts", diff --git a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/-.test.ts b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/-.test.ts index ae4283020b..ee07197fc3 100644 --- a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/-.test.ts +++ b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/-.test.ts @@ -56,7 +56,7 @@ describe('DenoFile', () => { describe('path', () => { it('from path: exists', async () => { const res = await DenoFile.workspace(rootPath); - const dirs = res.children.map((child) => Fs.dirname(child.path)); + const dirs = res.children.map((child) => child.path.dir); expect(res.exists).to.eql(true); expect(dirs.some((p) => p === 'code/sys/std')).to.be.true; expect(res.file).to.eql(rootPath); @@ -70,7 +70,7 @@ describe('DenoFile', () => { const dirs = root.data?.workspace?.map((p) => p.replace(/^\.\//, '')); expect(a.exists).to.eql(true); - expect(a.children.map((m) => Fs.dirname(m.path))).to.eql(dirs); + expect(a.children.map((m) => m.path.dir)).to.eql(dirs); expect(b.exists).to.eql(false); expect(b.children).to.eql([]); @@ -84,12 +84,27 @@ describe('DenoFile', () => { }); describe('workspace.children', () => { - it('children.files (paths)', async () => { + it('child.path', async () => { const ws = await DenoFile.workspace(); - const dirs = ws.children.map((child) => Fs.dirname(child.path)); + const dirs = ws.children.map((child) => child.path.dir); const paths = dirs.map((subdir) => Fs.join('./', subdir, 'deno.json')); ws.children.forEach((child) => { - expect(paths.includes(child.path)).to.eql(true); + expect(paths.includes(child.path.denofile)).to.eql(true); + }); + }); + + it('child.pkg', async () => { + const ws = await DenoFile.workspace(); + const match = ws.children.find((m) => m.denofile.name === pkg.name); + + expect(match).to.exist; + expect(match?.pkg).to.eql(pkg); + expect(match?.denofile.name).to.eql(pkg.name); + expect(match?.denofile.version).to.eql(pkg.version); + + ws.children.forEach((child) => { + expect(child.pkg.name).to.eql(child.denofile.name || '<unnamed>'); + expect(child.pkg.version).to.eql(child.denofile.version || '0.0.0'); }); }); }); @@ -100,7 +115,7 @@ describe('DenoFile', () => { expect(ws.modules.ok).to.eql(true); expect(ws.modules.error).to.eql(undefined); - const namesA = ws.children.map((m) => m.file.name ?? ''); + const namesA = ws.children.map((m) => m.denofile.name ?? ''); const namesB = ws.modules.items.map((m) => m.name); expect(namesA.filter(Boolean).toSorted()).to.eql(namesB.filter(Boolean).toSorted()); }); diff --git a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.Workspace.ts b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.Workspace.ts index abf42e21e0..11958b11eb 100644 --- a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.Workspace.ts +++ b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.Workspace.ts @@ -15,6 +15,7 @@ export type DenoWorkspace = { * Represents a single child of a workspace. */ export type DenoWorkspaceChild = { - readonly file: t.DenoFileJson; - readonly path: t.StringPath; + readonly path: { readonly dir: t.StringDir; readonly denofile: t.StringPath }; + readonly denofile: t.DenoFileJson; + readonly pkg: t.Pkg; }; diff --git a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.ts b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.ts index c0c95ffb3e..3bcc283ced 100644 --- a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.ts +++ b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/t.ts @@ -39,7 +39,15 @@ export type DenoFileJson = { licence?: string; tasks?: Record<string, string>; importMap?: t.StringPath; - imports?: Record<string, string>; + imports?: Record<string, t.StringModuleSpecifier>; exports?: Record<string, string>; workspace?: t.StringPath[]; }; + +/** + * A JSON file containing an import-map. + * Referenced by `importMap` path in `deno.json` file. + */ +export type DenoImportMapJson = { + imports?: Record<string, t.StringModuleSpecifier>; +}; diff --git a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/u.workspace.ts b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/u.workspace.ts index 7f10d27354..40c5312787 100644 --- a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/u.workspace.ts +++ b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoFile/u.workspace.ts @@ -32,7 +32,7 @@ export const workspace: t.DenoFileLib['workspace'] = async (path, options = {}) file, children, get modules() { - return _modules || (_modules = Esm.modules(toModuleSpecifiers(children.map((m) => m.file)))); + return _modules || (_modules = Esm.modules(toSpecifiers(children.map((m) => m.denofile)))); }, }; return api; @@ -72,12 +72,22 @@ async function loadFiles(root: t.StringDir, subpaths: t.StringPath[]) { const promises = subpaths .map((subpath) => Path.join(root, subpath, 'deno.json')) .map((path) => load(path)); + + const toChild = (path: t.StringPath, denofile: t.DenoFileJson): t.DenoWorkspaceChild => { + const dir = Path.dirname(path); + return { + path: { dir, denofile: path }, + pkg: { name: denofile.name ?? '<unnamed>', version: denofile.version ?? '0.0.0' }, + denofile, + }; + }; + return (await Promise.all(promises)) - .map((m): t.DenoWorkspaceChild => ({ file: m.data!, path: trimPath(m.path) })) - .filter((m) => !!m.file); + .filter((m) => !!m.data) + .map((m) => toChild(trimPath(m.path), m.data!)); } -function toModuleSpecifiers(files: t.DenoFileJson[]) { +function toSpecifiers(files: t.DenoFileJson[]): t.StringModuleSpecifier[] { return files .filter((file) => !!file.name) .map((file) => { diff --git a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/-backup.test.ts b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/-backup.test.ts index 4fc1ebd531..e18ebb4b5c 100644 --- a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/-backup.test.ts +++ b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/-backup.test.ts @@ -29,7 +29,7 @@ describe('DenoModule.backup', () => { // Copy in sample project-files to create snapshot/backup of. const tmpl = SAMPLE.sample1.tmpl(); - await tmpl.copy(source); + await tmpl.write(source); await assertExists(Fs.join(source, PATHS.dist), true); await assertExists(Fs.join(source, PATHS.tmp), true); @@ -68,7 +68,7 @@ describe('DenoModule.backup', () => { const sample = sampleFs(); const source = sample.dir; - await SAMPLE.sample1.tmpl().copy(source); + await SAMPLE.sample1.tmpl().write(source); await assertExists(Fs.join(source, PATHS.dist), true); await assertExists(Fs.join(source, PATHS.tmp), true); @@ -100,7 +100,7 @@ describe('DenoModule.backup', () => { const sample = sampleFs(); const source = sample.dir; - await SAMPLE.sample1.tmpl().copy(source); + await SAMPLE.sample1.tmpl().write(source); await assertExists(Fs.join(source, PATHS.dist), true); await assertExists(Fs.join(source, PATHS.tmp), true); @@ -131,7 +131,7 @@ describe('DenoModule.backup', () => { it('{message} param ā commit details', async () => { const sample = sampleFs(); const source = sample.dir; - await SAMPLE.sample1.tmpl().copy(source); + await SAMPLE.sample1.tmpl().write(source); const message = 'š hello'; diff --git a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/u.upgrade.ts b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/u.upgrade.ts index c3b7a593e3..76e7f09fc0 100644 --- a/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/u.upgrade.ts +++ b/code/sys.driver/driver-deno/src/ns.Runtime/m.DenoModule/u.upgrade.ts @@ -79,7 +79,7 @@ export const upgrade: t.DenoModuleLib['upgrade'] = async (args) => { /** * Finish up. */ - const fmtTargetVer = c.bold(c.green(Semver.toString(targetVersion))); + const fmtTargetVer = c.bold(c.brightCyan(Semver.toString(targetVersion))); console.info(c.green(`Project at version:`)); console.info(c.gray(`${c.white(c.bold(moduleName))}@${fmtTargetVer}`)); console.info(); diff --git a/code/sys.driver/driver-deno/src/pkg.ts b/code/sys.driver/driver-deno/src/pkg.ts index 79cb3f9c80..c7559a5430 100644 --- a/code/sys.driver/driver-deno/src/pkg.ts +++ b/code/sys.driver/driver-deno/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-deno', version: '0.0.97' }; diff --git a/code/sys.driver/driver-immer/deno.json b/code/sys.driver/driver-immer/deno.json index f492539ba2..901fa8136e 100644 --- a/code/sys.driver/driver-immer/deno.json +++ b/code/sys.driver/driver-immer/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-immer", - "version": "0.0.86", + "version": "0.0.96", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys.driver/driver-immer/src/pkg.ts b/code/sys.driver/driver-immer/src/pkg.ts index 79cb3f9c80..3b547d69b0 100644 --- a/code/sys.driver/driver-immer/src/pkg.ts +++ b/code/sys.driver/driver-immer/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-immer', version: '0.0.96' }; diff --git a/code/sys.driver/driver-obsidian/deno.json b/code/sys.driver/driver-obsidian/deno.json index 833d9b3c86..98b4711bdf 100644 --- a/code/sys.driver/driver-obsidian/deno.json +++ b/code/sys.driver/driver-obsidian/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-obsidian", - "version": "0.0.73", + "version": "0.0.83", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys.driver/driver-obsidian/src/pkg.ts b/code/sys.driver/driver-obsidian/src/pkg.ts index 79cb3f9c80..7ebb194ea7 100644 --- a/code/sys.driver/driver-obsidian/src/pkg.ts +++ b/code/sys.driver/driver-obsidian/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-obsidian', version: '0.0.83' }; diff --git a/code/sys.driver/driver-ollama/deno.json b/code/sys.driver/driver-ollama/deno.json index 389782e25e..ee24136e57 100644 --- a/code/sys.driver/driver-ollama/deno.json +++ b/code/sys.driver/driver-ollama/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-ollama", - "version": "0.0.40", + "version": "0.0.50", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys.driver/driver-ollama/src/pkg.ts b/code/sys.driver/driver-ollama/src/pkg.ts index 79cb3f9c80..147f053698 100644 --- a/code/sys.driver/driver-ollama/src/pkg.ts +++ b/code/sys.driver/driver-ollama/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-ollama', version: '0.0.50' }; diff --git a/code/sys.driver/driver-orbiter/deno.json b/code/sys.driver/driver-orbiter/deno.json index ed6855b66a..66cb50f16c 100644 --- a/code/sys.driver/driver-orbiter/deno.json +++ b/code/sys.driver/driver-orbiter/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-orbiter", - "version": "0.0.63", + "version": "0.0.73", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys.driver/driver-orbiter/src/pkg.ts b/code/sys.driver/driver-orbiter/src/pkg.ts index 79cb3f9c80..4c9c503e67 100644 --- a/code/sys.driver/driver-orbiter/src/pkg.ts +++ b/code/sys.driver/driver-orbiter/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-orbiter', version: '0.0.73' }; diff --git a/code/sys.driver/driver-quilibrium/deno.json b/code/sys.driver/driver-quilibrium/deno.json index 42b86ece8c..19c4ba27cb 100644 --- a/code/sys.driver/driver-quilibrium/deno.json +++ b/code/sys.driver/driver-quilibrium/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/driver-quilibrium", - "version": "0.0.76", + "version": "0.0.86", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys.driver/driver-quilibrium/src/pkg.ts b/code/sys.driver/driver-quilibrium/src/pkg.ts index 79cb3f9c80..23f25f9ea0 100644 --- a/code/sys.driver/driver-quilibrium/src/pkg.ts +++ b/code/sys.driver/driver-quilibrium/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-quilibrium', version: '0.0.86' }; diff --git a/code/sys.driver/driver-vite/-scripts/-build.ts b/code/sys.driver/driver-vite/-scripts/-build.ts deleted file mode 100644 index ab8d18ca3b..0000000000 --- a/code/sys.driver/driver-vite/-scripts/-build.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Run in a child-process (hence the `-allow-run` requirement). - */ -import { Vite } from '@sys/driver-vite'; -import { pkg } from '../src/pkg.ts'; - -// const input = './src/-test/vite.sample-1/main.ts'; -const input = './src/-test/vite.sample-2/index.html'; -// const input = './src/-test/vite.sample-3/main.ts'; - -const bundle = await Vite.build({ pkg, input }); - -console.log(`-------------------------------------------`); -console.info(bundle.toString({ pad: true })); diff --git a/code/sys.driver/driver-vite/-scripts/-main.ts b/code/sys.driver/driver-vite/-scripts/-main.ts index dbb18b8049..d19bf9754b 100644 --- a/code/sys.driver/driver-vite/-scripts/-main.ts +++ b/code/sys.driver/driver-vite/-scripts/-main.ts @@ -1,10 +1,5 @@ /** - * Sample used to pass "deno task" commands to the entry point. - * - * @example - * Invoked via deno run command: - * ```bash - * deno run jsr:@sys/http/server/start --port=1234 - * ``` + * Sample used to pass "deno task" commands to the entry point + * locally within the mono-repo rather than calling out to JSR. */ import '../src/-entry/-main.ts'; diff --git a/code/sys.driver/driver-vite/-scripts/-prep.ts b/code/sys.driver/driver-vite/-scripts/-prep.ts index b8dbaffbe4..3ff9a84aeb 100644 --- a/code/sys.driver/driver-vite/-scripts/-prep.ts +++ b/code/sys.driver/driver-vite/-scripts/-prep.ts @@ -1,25 +1,22 @@ import { Vite } from '@sys/driver-vite'; -import { type t, DenoDeps, DenoFile, Fs, PATHS, c, pkg } from './common.ts'; +import { c, Fs, PATHS, pkg, Semver } from './common.ts'; const resolve = (...parts: string[]) => Fs.join(import.meta.dirname ?? '', '..', ...parts); await Fs.remove(resolve('.tmp')); /** - * Save monorepo deps. + * Update to latest dependency versions. */ -const ws = await DenoFile.workspace(); -const deps: t.Dep[] = ws.modules.items.map((esm) => DenoDeps.toDep(esm)); - -const dir = resolve('src/-tmpl/.sys'); -await Fs.copy(Fs.join(ws.dir, 'deps.yaml'), Fs.join(dir, 'deps.yaml'), { force: true }); -await Fs.write(Fs.join(dir, 'deps.sys.yaml'), DenoDeps.toYaml(deps).text); +await Vite.Tmpl.prep(); /** - * Bundle files (for code-registry). + * Bundle files inline, base64-string FileMap (NB: so code can be referenfed within the registry). */ const bundle = Vite.Tmpl.Bundle; await bundle.toFilemap(); await bundle.writeToFile(resolve(PATHS.tmpl.tmp)); // NB: test output. -console.info(c.brightCyan('ā Prep Complete:'), `${pkg.name}@${c.brightCyan(pkg.version)}`); +const fmtVersion = Semver.Fmt.colorize(pkg.version); +const fmtModule = `${pkg.name}${c.dim('@')}${fmtVersion}`; +console.info(c.brightCyan('ā Prep Complete:'), fmtModule); console.info(); diff --git a/code/sys.driver/driver-vite/-scripts/-reset.ts b/code/sys.driver/driver-vite/-scripts/-reset.ts index c22fe5afba..55ab57446e 100644 --- a/code/sys.driver/driver-vite/-scripts/-reset.ts +++ b/code/sys.driver/driver-vite/-scripts/-reset.ts @@ -1,6 +1,7 @@ -import { Fs } from '@sys/fs'; +import { Fs } from './common.ts'; const remove = (path: string) => Fs.remove(Fs.resolve(path), { log: true }); await remove('./dist'); await remove('./.tmp'); +await remove('./.swc'); await remove('./node_modules'); diff --git a/code/sys.driver/driver-vite/-scripts/-tmp.ts b/code/sys.driver/driver-vite/-scripts/-tmp.ts index abe06e35d8..e133d73e74 100644 --- a/code/sys.driver/driver-vite/-scripts/-tmp.ts +++ b/code/sys.driver/driver-vite/-scripts/-tmp.ts @@ -1,4 +1,4 @@ -import { Fs } from '@sys/fs'; +import { Fs } from './common.ts'; console.log('tmp š·\n'); diff --git a/code/sys.driver/driver-vite/deno.json b/code/sys.driver/driver-vite/deno.json index f25a2cfe9d..93585b1acf 100644 --- a/code/sys.driver/driver-vite/deno.json +++ b/code/sys.driver/driver-vite/deno.json @@ -1,10 +1,11 @@ { "name": "@sys/driver-vite", - "version": "0.0.121", + "version": "0.0.137", "license": "MIT", "tasks": { "lint": "deno lint", "dry": "deno publish --allow-dirty --dry-run", + "check": "deno check src/*", "test": "deno test -RWNE --allow-run --allow-ffi --allow-sys", "reset": "deno run -RWE ./-scripts/-reset.ts", "init": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=init --dir=./.tmp/sample", diff --git a/code/sys.driver/driver-vite/src/-entry/m.Entry.ts b/code/sys.driver/driver-vite/src/-entry/m.Entry.ts index b3f1006faa..021b777b41 100644 --- a/code/sys.driver/driver-vite/src/-entry/m.Entry.ts +++ b/code/sys.driver/driver-vite/src/-entry/m.Entry.ts @@ -9,17 +9,21 @@ import { build } from './u.build.ts'; import { dev } from './u.dev.ts'; import { serve } from './u.serve.ts'; +type O = Record<string, unknown>; + export const ViteEntry: t.ViteEntryLib = { dev, build, serve, async main(input) { - const args = wrangle.args(input ?? Deno.args); + const argsAsType = <T extends O>() => wrangle.args<T>((input ?? Deno.args) as string[]); + const args = argsAsType<t.ViteEntryArgs>(); const cmd = args.cmd; if (cmd === 'init') { const { init } = await import('./u.init.ts'); + console.log('args', args); await init(args); return; } @@ -94,8 +98,7 @@ export const ViteEntry: t.ViteEntryLib = { * Helpers */ const wrangle = { - args(argv: string[] | t.ViteEntryArgs) { - type T = t.ViteEntryArgs; + args<T extends O>(argv: string[] | T) { return Array.isArray(argv) ? Args.parse<T>(argv) : (argv as T); }, } as const; diff --git a/code/sys.driver/driver-vite/src/-entry/t.ts b/code/sys.driver/driver-vite/src/-entry/t.ts index a2041a22ba..2bb545c058 100644 --- a/code/sys.driver/driver-vite/src/-entry/t.ts +++ b/code/sys.driver/driver-vite/src/-entry/t.ts @@ -33,7 +33,12 @@ export type ViteEntryArgs = | ViteEntryArgsHelp; /** The `init` command. */ -export type ViteEntryArgsInit = { cmd: 'init'; dir?: P; silent?: boolean }; +export type ViteEntryArgsInit = { + cmd: 'init'; + dir?: P; + silent?: boolean; + tmpl?: t.ViteTmplKind | boolean; +}; /** The `clean` command. */ export type ViteEntryArgsClean = { cmd: 'clean'; dir?: P }; diff --git a/code/sys.driver/driver-vite/src/-entry/u.build.ts b/code/sys.driver/driver-vite/src/-entry/u.build.ts index d098bcbe48..c3a9c1c761 100644 --- a/code/sys.driver/driver-vite/src/-entry/u.build.ts +++ b/code/sys.driver/driver-vite/src/-entry/u.build.ts @@ -1,4 +1,4 @@ -import { type t, Cli, Path, pkg, Vite } from './common.ts'; +import { type t, Path, pkg, Vite } from './common.ts'; /** * Run a local HTTP server from entry command-args. @@ -8,12 +8,10 @@ export async function build(args: t.ViteEntryArgsBuild) { if (args.cmd !== 'build') return; if (!silent) console.info(); - const spinner = Cli.Spinner.create('building', { silent }); const cwd = args.dir ? Path.resolve(args.dir) : Path.cwd(); - const bundle = await Vite.build({ cwd, pkg, silent }); + const bundle = await Vite.build({ cwd, pkg, silent, spinner: true }); if (!silent) { - spinner.stop(); console.info(bundle.toString({ pad: true })); } } diff --git a/code/sys.driver/driver-vite/src/-entry/u.init.ts b/code/sys.driver/driver-vite/src/-entry/u.init.ts index 27396f1080..f450c54bab 100644 --- a/code/sys.driver/driver-vite/src/-entry/u.init.ts +++ b/code/sys.driver/driver-vite/src/-entry/u.init.ts @@ -1,25 +1,57 @@ import { Vite } from '../m.Vite/mod.ts'; -import { type t, c, pkg, ViteLog } from './common.ts'; +import { type t, c, pkg, Semver, ViteLog, V } from './common.ts'; + +/** + * Args validation: + */ +export const TmplKindSchema = V.union([V.literal('Default'), V.literal('ComponentLib')]); +export const InitSchema = V.object({ + cmd: V.literal('init'), + dir: V.optional(V.string()), + silent: V.optional(V.boolean()), + tmpl: V.optional(TmplKindSchema, 'Default'), +}); /** * Run the initialization templates. */ export async function init(args: t.ViteEntryArgsInit) { - const { silent = false } = args; if (args.cmd !== 'init') return; + const { silent = false } = args; if (!silent) { console.info(); console.info(`${pkg.name} ${c.gray(pkg.version)}`); } - await Vite.Tmpl.update({ in: args.dir, silent }); + await Vite.Tmpl.write({ + in: args.dir, + tmpl: wrangle.tmplKind(args), + silent, + }); if (!silent) { console.info(); ViteLog.API.log(); + + const fmtVersion = Semver.Fmt.colorize(pkg.version); + const fmtModule = `${pkg.name}${c.dim('@')}${fmtVersion}`; + console.info(); - console.info(c.brightCyan('ā Init Complete:'), `${pkg.name}@${c.brightCyan(pkg.version)}`); + console.info(c.brightCyan('ā Init Complete:'), `${fmtModule}`); console.info(); } } + +/** + * Helpers + */ +const wrangle = { + tmplKind(args: t.ViteEntryArgsInit): t.ViteTmplKind { + if (!args.tmpl || args.tmpl === true) return 'Default'; + const validated = V.safeParse(TmplKindSchema, args.tmpl); + const issues = validated.issues; + issues?.forEach((issue) => console.warn(c.yellow('Parse Error: --tmpl:'), issue.message)); + return issues ? 'Default' : validated.output; + }, +} as const; diff --git a/code/sys.driver/driver-vite/src/-entry/u.serve.ts b/code/sys.driver/driver-vite/src/-entry/u.serve.ts index 8d07e4a9d5..89bef6b7a5 100644 --- a/code/sys.driver/driver-vite/src/-entry/u.serve.ts +++ b/code/sys.driver/driver-vite/src/-entry/u.serve.ts @@ -15,9 +15,9 @@ export async function serve(args: t.ViteEntryArgsServe) { const app = HttpServer.create({ pkg, hash, static: ['/*', dir] }); const options = HttpServer.options({ port, pkg, hash, silent }); - const fmtDir = c.gray(dir.replace(/^\.\//, '')); + const fmtDir = c.gray(dir.replace(/^\.\//, '').replace(/\/$/, '')); const fmtDirExists = c.yellow(!dirExists ? c.bold('(does not exist)') : ''); - console.info(c.gray(`Folder: ${fmtDir} ${fmtDirExists}`)); + console.info(c.gray(`Static: ${fmtDir}/ ${fmtDirExists}`)); Deno.serve(options, app.fetch); await HttpServer.keyboard({ port, print: !silent }); diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-1/main.tsx b/code/sys.driver/driver-vite/src/-test/vite.sample-1/main.tsx index d1021fa8db..5ca3d65a92 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-1/main.tsx +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-1/main.tsx @@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'; /** * Sample: render react component. */ -const root = createRoot(document.getElementById('root')); +const root = createRoot(document.getElementById('root')!); root.render( <StrictMode> <div>Hello World š</div> diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-1/vite.config.ts b/code/sys.driver/driver-vite/src/-test/vite.sample-1/vite.config.ts index 7c6b9851d1..ecc0c9967d 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-1/vite.config.ts +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-1/vite.config.ts @@ -1,11 +1,9 @@ -import { Vite } from '@sys/driver-vite'; -import { defineConfig } from 'vite'; - -export const paths = Vite.Config.paths(import.meta.url); +import { Vite } from 'jsr:@sys/driver-vite'; +import { defineConfig } from 'npm:vite'; export default defineConfig(() => Vite.Config.app({ - paths, + paths: Vite.Config.paths(), chunks(e) { e.chunk('react', 'react'); e.chunk('react.dom', 'react-dom'); diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/-entry/main.tsx b/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/-entry/main.tsx index 6d8aa959d6..16ec9cc25a 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/-entry/main.tsx +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/-entry/main.tsx @@ -16,7 +16,7 @@ dynamic.then((mod) => console.info('š¦ dynmaic import', mod)); /** * Sample: render react component. */ -const root = createRoot(document.getElementById('root')); +const root = createRoot(document.getElementById('root')!); root.render( <StrictMode> <View style={{ border: `solid 1px blue` }} /> diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/common/t.ts b/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/common/t.ts index 9e4786784f..00b8efd553 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/common/t.ts +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/common/t.ts @@ -1,2 +1,2 @@ export type { CommonTheme } from '@sys/types'; -export type { CssValue } from '@sys/ui-css/t'; +export type { CssInput } from '@sys/ui-css/t'; diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/mod.ts b/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/mod.ts new file mode 100644 index 0000000000..8e8dc7889b --- /dev/null +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-2/src/mod.ts @@ -0,0 +1 @@ +export { Foo } from './m.foo.ts'; diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-2/vite.config.ts b/code/sys.driver/driver-vite/src/-test/vite.sample-2/vite.config.ts index b876bdb85b..4b0084c50e 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-2/vite.config.ts +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-2/vite.config.ts @@ -1,8 +1,8 @@ -import { Vite } from '@sys/driver-vite'; -import { defineConfig } from 'vite'; +import { Vite } from 'jsr:@sys/driver-vite'; +import { defineConfig } from 'npm:vite'; export const paths = Vite.Config.paths({ - cwd: import.meta.url, + // cwd: import.meta.url, app: { entry: 'src/-entry/index.html' }, }); diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-config/custom/vite.config.ts b/code/sys.driver/driver-vite/src/-test/vite.sample-config/custom/vite.config.ts index 2fb9fa59da..219c506fe2 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-config/custom/vite.config.ts +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-config/custom/vite.config.ts @@ -2,17 +2,17 @@ import { Vite } from '@sys/driver-vite'; import { defineConfig } from 'vite'; -export const paths = Vite.Config.paths({ - app: { - entry: '.tmp/sample/src/-test/index.html', - outDir: '.tmp/sample/dist', - }, -}); - /** * SAMPLE: Custom plugin (no customization). */ export default defineConfig(async () => { + const paths = Vite.Config.paths({ + app: { + entry: '.tmp/sample/src/-test/index.html', + outDir: '.tmp/sample/dist', + }, + }); + const app = await Vite.Config.app({ paths, diff --git a/code/sys.driver/driver-vite/src/-test/vite.sample-config/simple/vite.config.ts b/code/sys.driver/driver-vite/src/-test/vite.sample-config/simple/vite.config.ts index 06cce588fa..b1d70cc137 100644 --- a/code/sys.driver/driver-vite/src/-test/vite.sample-config/simple/vite.config.ts +++ b/code/sys.driver/driver-vite/src/-test/vite.sample-config/simple/vite.config.ts @@ -1,7 +1,7 @@ -import { Vite } from '@sys/driver-vite'; -import { defineConfig } from 'vite'; +import { Vite } from 'jsr:@sys/driver-vite'; +import { defineConfig } from 'npm:vite'; -export const paths = Vite.Config.paths({ +const paths = Vite.Config.paths({ app: { entry: '.tmp/sample/src/-test/index.html', outDir: '.tmp/sample/dist', diff --git a/code/sys.driver/driver-vite/src/-tmpl/-scripts/-tmp.ts b/code/sys.driver/driver-vite/src/-tmpl/-scripts/-tmp.ts index 9e0f27eee7..ba662b31d4 100644 --- a/code/sys.driver/driver-vite/src/-tmpl/-scripts/-tmp.ts +++ b/code/sys.driver/driver-vite/src/-tmpl/-scripts/-tmp.ts @@ -1 +1 @@ -console.info('š', import.meta.url); +console.info(`\nš ${import.meta.url}\n`); diff --git a/code/sys.driver/driver-vite/src/-tmpl/.sys/deps.sys.yaml b/code/sys.driver/driver-vite/src/-tmpl/.sys/deps.sys.yaml deleted file mode 100644 index 290c766cd9..0000000000 --- a/code/sys.driver/driver-vite/src/-tmpl/.sys/deps.sys.yaml +++ /dev/null @@ -1,36 +0,0 @@ -deno.json: - - import: jsr:@sys/types@0.0.82 - - import: jsr:@sys/std@0.0.131 - - import: jsr:@sys/color@0.0.35 - - import: jsr:@sys/testing@0.0.75 - - import: jsr:@sys/fs@0.0.79 - - import: jsr:@sys/cli@0.0.64 - - import: jsr:@sys/process@0.0.65 - - import: jsr:@sys/crypto@0.0.64 - - import: jsr:@sys/http@0.0.46 - - import: jsr:@sys/text@0.0.73 - - import: jsr:@sys/tmpl@0.0.79 - - import: jsr:@sys/cmd@0.0.80 - - import: jsr:@sys/jsr@0.0.45 - - import: jsr:@sys/ui-css@0.0.70 - - import: jsr:@sys/ui-dom@0.0.77 - - import: jsr:@sys/ui-react@0.0.84 - - import: jsr:@sys/ui-react-devharness@0.0.82 - - import: jsr:@sys/ui-react-components@0.0.38 - - import: jsr:@sys/driver-automerge@0.0.84 - - import: jsr:@sys/driver-deno@0.0.86 - - import: jsr:@sys/driver-immer@0.0.86 - - import: jsr:@sys/driver-obsidian@0.0.73 - - import: jsr:@sys/driver-ollama@0.0.40 - - import: jsr:@sys/driver-orbiter@0.0.63 - - import: jsr:@sys/driver-quilibrium@0.0.76 - - import: jsr:@sys/driver-vite@0.0.121 - - import: jsr:@sys/driver-vitepress@0.0.284-alpha.2 - - import: jsr:@sys/sys@0.0.55 - - import: jsr:@sys/main@0.0.57 - - import: jsr:@tdb/api@0.0.72 - - import: jsr:@tdb/slc@0.0.68 - - import: jsr:@tdb/tmp@0.0.72 - - import: jsr:@sys/name@0.0.0 - - import: jsr:@sys/tmp@0.0.95 -package.json: [] diff --git a/code/sys.driver/driver-vite/src/-tmpl/.sys/deps.yaml b/code/sys.driver/driver-vite/src/-tmpl/.sys/deps.yaml deleted file mode 100644 index 9bd188f371..0000000000 --- a/code/sys.driver/driver-vite/src/-tmpl/.sys/deps.yaml +++ /dev/null @@ -1,117 +0,0 @@ -# -# System Dependencies ("imports") -# -# ./š¦ -# | deno.json -# |(write) ā deno.imports.json -# |(write) ā package.json -# -# This is the "single-source-of-truth" with regards to dependencies and versioning. -# Import maps (in the `deno.json` and `package.json` files) are auto-generated -# from this config definition. -# -# Also, as a programmatic API, other downstream dependencies -# (such as template generators, see `@sys/tmpl`) use this definition -# file to calculate the "latest" versions to inject into, say, -# a `package.json` file for a scaffolded project. -# - -groups: - std/deno: - # Deno standard libs ("std"). - - import: jsr:@std/async@1.0.10 - - import: jsr:@std/datetime@0.225.3 - - import: jsr:@std/dotenv@0.225.3 - - import: jsr:@std/encoding@1.0.7 - - import: jsr:@std/fs@1.0.13 - - import: jsr:@std/path@1.0.8 - - import: jsr:@std/semver@1.0.4 - - import: jsr:@std/testing@1.0.9 - - import: jsr:@std/uuid@1.0.4 - - automerge: - # https://automerge.org - - import: npm:@automerge/automerge@2.2.8 - - import: npm:@automerge/automerge-repo@1.2.1 - - import: npm:@automerge/automerge-repo-network-broadcastchannel@1.2.1 - - import: npm:@automerge/automerge-repo-storage-indexeddb@1.2.1 - - import: npm:@automerge/automerge-repo-storage-nodefs@1.2.1 - - import: npm:@onsetsoftware/automerge-patcher@0.14.0 - - crypto: - - import: npm:@noble/hashes@1.7.1 - wildcard: true - - build/tools: - - import: npm:@vitejs/plugin-react-swc@3.8.0 - - import: npm:rollup@4.34.8 - - import: npm:vite@6.1.1 - - import: npm:vite-plugin-wasm@3.4.1 - - ui/react: - - import: npm:@types/react@18.3.18 - - import: npm:@types/react-dom@18.3.5 - - import: npm:react@18.3.1 - - import: npm:react-dom@18.3.1 - -deno.json: - - group: std/deno - - group: crypto - - group: automerge - - # CLI tools - - import: jsr:@cliffy/keypress@1.0.0-rc.7 - - import: jsr:@cliffy/prompt@1.0.0-rc.7 - - import: jsr:@cliffy/table@1.0.0-rc.7 - - # Sundry: NPM - - import: npm:@types/diff@7.0.1 - - import: npm:chai@5 - - import: npm:approx-string-match@2 - - import: npm:date-fns@4 - - import: npm:subhosting@0.1.0-alpha.1 - - import: npm:diff@7 - - import: npm:fast-json-patch@3.1.1 - - import: npm:fake-indexeddb@6.0.0 - - import: npm:happy-dom@17.1.3 - - import: npm:hash-it@6.0.0 - - import: npm:ignore@7 - - import: npm:immer@10 - - import: npm:ora@8.2.0 - - import: npm:ollama@0.5.13 - - import: npm:pretty-bytes@6.1.1 - - import: npm:ramda@0.30.1 - - import: npm:rambda@9.4.2 - - import: npm:rxjs@7.8.2 - - import: npm:strip-ansi@7 - - import: npm:subhosting@0.1.0-alpha.1 - - import: npm:tinycolor2@1.6.0 - - import: npm:ts-essentials@10.0.4 - - import: npm:valibot@1.0.0-rc.1 - - import: npm:yaml@2.7.0 - - # Browser - - import: npm:csstype@3 - - import: npm:ua-parser-js@2.0.2 - - # UI - - import: npm:react-error-boundary@5 - - import: npm:react-inspector@6 - - import: npm:react-spinners@0.15.0 - -package.json: - - group: std/deno - - group: crypto - - group: build/tools - dev: true - - - import: npm:hono@4.7.2 - - # UI - - group: ui/react - - import: npm:react-icons@5.5.0 - - import: npm:@vidstack/react@1.12.12 - - # UI:Frameworks - - import: npm:vitepress@1.6.3 - - import: npm:vue@3.5.13 diff --git a/code/sys.driver/driver-vite/src/-tmpl/deno.json b/code/sys.driver/driver-vite/src/-tmpl/deno.json index f5408b5de7..77f03332f5 100644 --- a/code/sys.driver/driver-vite/src/-tmpl/deno.json +++ b/code/sys.driver/driver-vite/src/-tmpl/deno.json @@ -1,30 +1,27 @@ { "version": "0.0.0", - "license": "MIT", "tasks": { "dev": "deno run -RWNE --allow-run --allow-ffi <ENTRY> --cmd=dev --in=./src/-test/index.html", "build": "deno run -RWE --allow-run --allow-ffi <ENTRY> --cmd=build --in=./src/-test/index.html", "serve": "deno run -RNE --allow-run --allow-ffi <ENTRY> --cmd=serve", + "test": "deno test -RWNE --allow-run --allow-ffi --allow-sys", "clean": "deno run -RWE --allow-ffi <ENTRY> --cmd=clean", "upgrade": "deno run -RWNE --allow-run --allow-ffi <ENTRY> --cmd=upgrade", "backup": "deno run -RWE --allow-run --allow-ffi <ENTRY> --cmd=backup", "help": "deno run -RE --allow-ffi <ENTRY> --cmd=help", - "sys": "deno run -RWNE <ENTRY_SYS>", "tmp": "deno run -A ./-scripts/-tmp.ts" }, "compilerOptions": { "strict": true, "lib": ["deno.ns", "esnext", "dom", "dom.iterable", "dom.asynciterable"], "types": ["vite/client", "@types/react"], - "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment" + "jsx": "react-jsx", + "jsxImportSource": "react" }, "workspace": [], + "importMap": "./imports.json", "nodeModulesDir": "auto", - "imports": { - "<SELF_IMPORT_NAME>": "<SELF_IMPORT_URI>" - } + "license": "MIT" } diff --git a/code/sys.driver/driver-vite/src/-tmpl/imports.json b/code/sys.driver/driver-vite/src/-tmpl/imports.json new file mode 100644 index 0000000000..68d5e7cd1f --- /dev/null +++ b/code/sys.driver/driver-vite/src/-tmpl/imports.json @@ -0,0 +1,17 @@ +{ + "imports": { + "@sys/std": "jsr:@sys/std@0.0.141", + "@sys/tmp": "jsr:@sys/tmp@0.0.110", + "@sys/types": "jsr:@sys/types@0.0.92", + "@sys/ui-css": "jsr:@sys/ui-css@0.0.81", + "@sys/driver-vite": "jsr:@sys/driver-vite@0.0.137", + "react": "npm:react@18.3.1", + "react-dom": "npm:react-dom@18.3.1", + "@types/react": "npm:@types/react@18.3.18", + "@types/react-dom": "npm:@types/react-dom@18.3.5", + "@vitejs/plugin-react-swc": "npm:@vitejs/plugin-react-swc@3.8.0", + "rollup": "npm:rollup@4.34.8", + "vite": "npm:vite@6.1.1", + "vite-plugin-wasm": "npm:vite-plugin-wasm@3.4.1" + } +} diff --git a/code/sys.driver/driver-vite/src/-tmpl/package.json b/code/sys.driver/driver-vite/src/-tmpl/package.json deleted file mode 100644 index 1eb5b9a268..0000000000 --- a/code/sys.driver/driver-vite/src/-tmpl/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "dependencies": { - "@sys/std": "npm:@jsr/sys__std", - "@sys/tmp": "npm:@jsr/sys__tmp", - "@sys/types": "npm:@jsr/sys__types", - "@sys/ui-css": "npm:@jsr/sys__ui-css", - "react": "", - "react-dom": "", - "valibot": "" - }, - "devDependencies": { - "@types/react": "", - "@types/react-dom": "", - "@vitejs/plugin-react-swc": "", - "rollup": "", - "vite": "", - "vite-plugin-wasm": "" - } -} diff --git a/code/sys.driver/driver-vite/src/-tmpl/src/-test/-sample/ui.Foo.tsx b/code/sys.driver/driver-vite/src/-tmpl/src/-test/-sample/ui.Foo.tsx index cc472ab2a9..056ddf70d8 100644 --- a/code/sys.driver/driver-vite/src/-tmpl/src/-test/-sample/ui.Foo.tsx +++ b/code/sys.driver/driver-vite/src/-tmpl/src/-test/-sample/ui.Foo.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Foo } from '@sys/tmp/ui'; +// import { Foo } from '@sys/tmp/ui'; import { Color, css } from '@sys/ui-css'; import type { t } from '../../common.ts'; @@ -70,7 +70,7 @@ export const FooSample: React.FC<FooComponent> = (props) => { <div>{`(see console for import samples)`}</div> <div style={{ paddingTop: 10 }}> {'Imported from ā '} - <Foo /> + {/* <Foo /> */} <div>{'š·š· TMP š·š· import WIP'}</div> <code>{'<JSX> ā Vite ā ESM.js.d.ts ā mod.ts ā JSR ā import'}</code> </div> diff --git a/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.lib.ts b/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.lib.ts deleted file mode 100644 index 4e406e6d67..0000000000 --- a/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.lib.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const fn = async () => { - const { Foo } = await import('./-sample/m.Foo.ts'); - const { FooSample } = await import('./-sample/ui.Foo.tsx'); - return { - Foo, - FooSample, - }; -}; diff --git a/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.tsx b/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.tsx index f5e0c12336..92d0030698 100644 --- a/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.tsx +++ b/code/sys.driver/driver-vite/src/-tmpl/src/-test/entry.tsx @@ -1,4 +1,4 @@ -import { pkg } from '../common.ts'; +import { pkg } from '../pkg.ts'; /** * Render UI. @@ -14,7 +14,7 @@ import { FooSample } from './-sample/ui.Foo.tsx'; * š· Test " @sys " module imports from across the * namespace (monorepo/workspace). */ -import '@sys/tmp/sample-imports'; +// import '@sys/tmp/sample-imports'; /** * Sample: render react component. diff --git a/code/sys.driver/driver-vite/src/-tmpl/vite.config.ts b/code/sys.driver/driver-vite/src/-tmpl/vite.config.ts index b9a82aaf68..dd19226f00 100644 --- a/code/sys.driver/driver-vite/src/-tmpl/vite.config.ts +++ b/code/sys.driver/driver-vite/src/-tmpl/vite.config.ts @@ -1,15 +1,16 @@ -import { Vite } from '@sys/driver-vite'; -import { defineConfig } from 'vite'; - -export const paths = Vite.Config.paths({ app: { entry: './src/-test/index.html' } }); +import { Vite } from 'jsr:@sys/driver-vite'; +import { defineConfig } from 'npm:vite'; export default defineConfig(() => { + const entry = './src/-test/index.html'; + const paths = Vite.Config.paths({ app: { entry } }); return Vite.Config.app({ paths, chunks(e) { e.chunk('react', 'react'); e.chunk('react.dom', 'react-dom'); e.chunk('sys', ['@sys/std']); + e.chunk('css', ['@sys/ui-css']); }, }); }); diff --git a/code/sys.driver/driver-vite/src/common/libs.ts b/code/sys.driver/driver-vite/src/common/libs.ts index 055644b352..2a79093661 100644 --- a/code/sys.driver/driver-vite/src/common/libs.ts +++ b/code/sys.driver/driver-vite/src/common/libs.ts @@ -1,9 +1,9 @@ /** * System */ -export { Args, asArray, Delete, Err, isRecord, R, slug, Str, Time } from '@sys/std'; +export { Args, asArray, Delete, Err, isRecord, R, slug, Str, Time, V } from '@sys/std'; export { Esm } from '@sys/std/esm'; -export { Semver } from '@sys/std/semver'; +export { Semver } from '@sys/std/semver/server'; export { c, Cli, stripAnsi } from '@sys/cli'; export { HashFmt } from '@sys/crypto/fmt'; diff --git a/code/sys.driver/driver-vite/src/common/mod.ts b/code/sys.driver/driver-vite/src/common/mod.ts index f02c42540c..8556c7dd22 100644 --- a/code/sys.driver/driver-vite/src/common/mod.ts +++ b/code/sys.driver/driver-vite/src/common/mod.ts @@ -2,8 +2,10 @@ export type * as t from './t.ts'; export { pkg } from '../pkg.ts'; export * from './libs.ts'; +export * from './u.workspace.ts'; export const PATHS = { + base: './', dist: 'dist/', backup: '-backup/', tmp: '.tmp/', diff --git a/code/sys.driver/driver-vite/src/common/t.ts b/code/sys.driver/driver-vite/src/common/t.ts index 9d4c2d6f96..afa6294df3 100644 --- a/code/sys.driver/driver-vite/src/common/t.ts +++ b/code/sys.driver/driver-vite/src/common/t.ts @@ -20,6 +20,7 @@ export type * from '@sys/std/t'; export type { DenoFileJson, DenoFilePath, + DenoImportMapJson, DenoModuleBackup, DenoWorkspace, DenoWorkspaceChild, @@ -27,7 +28,7 @@ export type { } from '@sys/driver-deno/t'; export type { FsPathFilter } from '@sys/fs/t'; export type { ProcHandle, ProcOutput, ProcReadySignalFilter } from '@sys/process/t'; -export type { Tmpl, TmplCopyHandler, TmplFileOperation, TmplProcessFile } from '@sys/tmpl/t'; +export type { Tmpl, TmplWriteHandler, TmplFileOperation, TmplProcessFile } from '@sys/tmpl/t'; export type { CssValue } from '@sys/ui-css/t'; /** diff --git a/code/sys.driver/driver-vite/src/common/u.workspace.ts b/code/sys.driver/driver-vite/src/common/u.workspace.ts new file mode 100644 index 0000000000..146c44d576 --- /dev/null +++ b/code/sys.driver/driver-vite/src/common/u.workspace.ts @@ -0,0 +1,18 @@ +import { DenoDeps, DenoFile, Path, Esm } from './libs.ts'; + +/** + * Find and load the latest workspace data. + */ +export async function getWorkspaceModules() { + const ws = await DenoFile.workspace(); + const deps = (await DenoDeps.from(Path.join(ws.dir, 'deps.yaml'))).data; + const modules = Esm.modules([...(deps?.modules.items ?? []), ...ws.modules.items]); + return { + get ws() { + return ws; + }, + get modules() { + return modules; + }, + } as const; +} diff --git a/code/sys.driver/driver-vite/src/m.Log/u.Bundle.ts b/code/sys.driver/driver-vite/src/m.Log/u.Bundle.ts index ccb2b976b7..167e63b6cd 100644 --- a/code/sys.driver/driver-vite/src/m.Log/u.Bundle.ts +++ b/code/sys.driver/driver-vite/src/m.Log/u.Bundle.ts @@ -1,5 +1,4 @@ -import { type t, c, Path, Str, Time } from './common.ts'; - +import { type t, c, Path, Semver, Str, Time } from './common.ts'; import { digest, pad } from './u.ts'; export const Bundle: t.ViteLogLib['Bundle'] = { @@ -24,8 +23,9 @@ ${c.gray(`out: ${outDir.replace(/\/$/, '')}/dist.json`)} ${tx} `; text = text.trim(); if (pkg) { - const mod = c.white(c.bold(pkg.name)); - text += c.gray(`\npkg: ${mod} ${c.cyan(c.bold(pkg.version))}`); + const fmtVersion = Semver.Fmt.colorize(pkg.version); + const fmtModule = `${c.white(c.bold(pkg.name))}${c.dim('@')}${fmtVersion}`; + text += c.gray(`\npkg: ${fmtModule}`); } return pad(text, args.pad); }, diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/-.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/-.test.ts index 335e180f31..54572f3504 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/-.test.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/-.test.ts @@ -8,7 +8,7 @@ describe('ViteConfig.workspace', () => { }); it('loads (via path)', async () => { - const map = (children: t.DenoWorkspaceChild[]) => children.map((m) => Fs.dirname(m.path)); + const map = (children: t.DenoWorkspaceChild[]) => children.map((m) => m.path.dir); const a = await workspace(); // NB: finds root workspace const b = await workspace({ denofile: ROOT.denofile.path }); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/mod.ts b/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/mod.ts index 34274f914d..415d57ba98 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/mod.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config.Workspace/mod.ts @@ -49,7 +49,7 @@ const wrangle = { }, async modules(base: t.StringDir, children: t.DenoWorkspaceChild[], filter?: t.WorkspaceFilter) { - const wait = children.map((child) => wrangle.exports(base, Fs.dirname(child.path), filter)); + const wait = children.map((child) => wrangle.exports(base, child.path.dir, filter)); const res = await Array.fromAsync(wait); return res .filter((item) => item.exists) diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/-.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/-.test.ts index 0afbfb4f49..e1098861ba 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/-.test.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/-.test.ts @@ -1,4 +1,4 @@ -import { type t, c, describe, expect, it, Path, SAMPLE } from '../-test.ts'; +import { type t, c, describe, expect, it } from '../-test.ts'; import { Vite } from '../mod.ts'; import { Is } from './m.Is.ts'; import { ViteConfig } from './mod.ts'; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/-app.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/-app.test.ts index ceb8666d9a..ec7e5d5373 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/-app.test.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/-app.test.ts @@ -2,7 +2,7 @@ import { type t, Fs, c, describe, expect, it } from '../-test.ts'; import { ViteConfig } from './mod.ts'; describe('Config.Build', () => { - const { brightCyan: cyan, bold } = c; + const { brightCyan: cyan } = c; describe('app (application)', () => { const includesPlugin = (config: t.ViteUserConfig, name: string) => { @@ -10,44 +10,57 @@ describe('Config.Build', () => { return plugins.some((p) => p.name === name); }; - it('default', async () => { - const p = ViteConfig.paths(); - const config = await ViteConfig.app(); - - expect(config.root).to.eql(p.cwd); - expect(config.build?.rollupOptions?.input).to.eql(Fs.join(p.cwd, p.app.entry)); - expect(config.build?.outDir).to.eql(Fs.join(p.cwd, p.app.outDir)); + const print = (config: t.ViteUserConfig, titleSuffix?: string, paths?: t.ViteConfigPaths) => { + if (paths) { + console.info(); + console.info(cyan(c.bold('ā INPUT paths'))); + console.info(paths); + console.info(); + } console.info(); - console.info(bold(cyan('ViteConfig.app (default):'))); + console.info(cyan(c.bold('ā ViteConfig.app')), c.gray(titleSuffix ?? '')); console.info({ ...config, plugins: ((config.plugins ?? []) as t.VitePlugin[]).flat().map((m) => m.name), resolve: { ...config.resolve, - alias: `ā š³ ${config.resolve?.alias?.length} total aliases`, + alias: `ā š³ ${config.resolve?.alias?.length} aliases (across workspace)`, }, }); console.info(); + return config; + }; + + it('defaults', async () => { + const p = ViteConfig.paths(); + const config = await ViteConfig.app(); + print(config, '(defaults)', p); + + expect(config.root).to.eql(p.cwd); + expect(config.build?.rollupOptions?.input).to.eql(Fs.join(p.cwd, p.app.entry)); + expect(config.build?.outDir).to.eql(Fs.join(p.cwd, p.app.outDir)); expect(includesPlugin(config, 'vite-plugin-wasm')).to.be.true; expect(includesPlugin(config, 'vite:react-swc')).to.be.true; }); it('no plugins', async () => { - const config = await ViteConfig.app({ plugins: { wasm: false, react: false } }); + const config = await ViteConfig.app({ plugins: { wasm: false, react: false, deno: false } }); expect(config.plugins).to.eql([]); }); it('custom paths', async () => { const paths = ViteConfig.paths({ - cwd: ' /foo/ ', - app: { entry: 'src/-foo.html', outDir: 'bar' }, + cwd: ' /foo/ ', // NB: absolute path (trimmed internally). + app: { entry: 'src/-foo.html', outDir: 'foobar/out' }, }); const config = await ViteConfig.app({ paths }); + print(config, '(custom paths)', paths); + expect(config.root).to.eql('/foo/src'); - expect(config.build?.rollupOptions?.input).to.eql(Fs.join(paths.cwd, paths.app.entry)); - expect(config.build?.outDir).to.eql(Fs.join(paths.cwd, paths.app.outDir)); + expect(config.build?.rollupOptions?.input).to.eql(Fs.join(paths.cwd, 'src/-foo.html')); + expect(config.build?.outDir).to.eql(Fs.join(paths.cwd, 'foobar/out')); }); }); }); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/-fromFile.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/-fromFile.test.ts new file mode 100644 index 0000000000..b734393319 --- /dev/null +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/-fromFile.test.ts @@ -0,0 +1,55 @@ +import { type t, c, describe, expect, it, Path, SAMPLE } from '../-test.ts'; +import { Vite } from '../mod.ts'; +import { ViteConfig } from './mod.ts'; + +describe('ViteConfig.fromFile', () => { + const { brightCyan: cyan } = c; + + const print = (res: t.ViteConfigFromFile) => { + console.info(); + console.info(cyan(`ā Type: ${c.bold('ViteConfigFromFile')}`)); + console.info(res); + console.info(); + }; + + it('from "/<root-dir>/"', async () => { + const rootDir = SAMPLE.Dirs.sample2; + const res = await ViteConfig.fromFile(rootDir); + print(res); + + expect(res.exists).to.eql(true); + expect(res.error).to.eql(undefined); + + expect(res.paths?.cwd).to.eql(Path.resolve(rootDir)); + expect(res.paths?.app.entry).to.eql('src/-entry/index.html'); + expect(res.paths?.app.base).to.eql('./'); + expect(res.paths?.app.outDir).to.eql('dist/'); + }); + + it('from "/<root-dir>/vite.config.ts" (with config filename)', async () => { + const rootDir = SAMPLE.Dirs.sample2; + const resA = await ViteConfig.fromFile(Path.join(rootDir, 'vite.config.ts')); + const resB = await ViteConfig.fromFile(rootDir); + expect(resA.exists).to.eql(true); + expect(resA.error).to.eql(undefined); + expect(resA).to.eql(resB); + }); + + it('loads main samples', async () => { + const test = async (path: t.StringPath) => { + const res = await Vite.Config.fromFile(path); + expect(res.error).to.eql(undefined); + expect(ViteConfig.Is.paths(res.paths)).to.be.true; + }; + + await test('src/-test/vite.sample-config/simple/vite.config.ts'); + await test('src/-test/vite.sample-config/custom/vite.config.ts'); + }); + + it('fail: not found', async () => { + const res = await ViteConfig.fromFile('/foo/404/vite.config.ts'); + expect(res.exists).to.eql(false); + expect(res.error?.message).to.include('A config file could not be found in directory'); + expect(res.error?.message).to.include(': /foo/404'); + }); +}); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/-paths.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/-paths.test.ts index 81457c9bbd..601bc81cbd 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/-paths.test.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/-paths.test.ts @@ -1,11 +1,10 @@ -import { type t, c, describe, expect, it, Path, SAMPLE } from '../-test.ts'; -import { Vite } from '../mod.ts'; +import { c, describe, expect, it, Path } from '../-test.ts'; import { ViteConfig } from './mod.ts'; -describe('ViteConfig: paths', () => { +describe('ViteConfig.paths', () => { const { brightCyan: cyan, bold } = c; - describe('ViteConfig.paths', () => { + describe('[ViteConfigPaths]: data structure', () => { it('default paths (empty params)', () => { const a = ViteConfig.paths(); const b = ViteConfig.paths(); @@ -65,52 +64,4 @@ describe('ViteConfig: paths', () => { expect(d.cwd).to.eql(b.cwd); }); }); - - describe('ViteConfig.fromFile', () => { - it('load from file path', async () => { - const rootDir = SAMPLE.Dirs.sample2; - const path = Path.join(rootDir, 'vite.config.ts'); - const res = await ViteConfig.fromFile(path); - - expect(res.error).to.eql(undefined); - expect(res.path).to.eql(Path.resolve(path)); - expect(res.exists).to.eql(true); - - const module = res.module; - expect(module.paths?.app.entry).to.eql('src/-entry/index.html'); - expect(res.module.paths?.cwd).to.eql(Path.resolve(rootDir)); - expect(typeof module.defineConfig === 'function').to.be.true; - }); - - it('no params: load from implicit {CWD}', async () => { - const res = await ViteConfig.fromFile(); - expect(res.path).to.eql(Path.resolve('vite.config.ts')); - expect(res.module.paths?.cwd).to.eql(Path.cwd()); - }); - - it('no `paths` | no `defineConfig`', async () => { - const path = Path.fromFileUrl(import.meta.url); // NB: not a `vite.config.ts` module. - const res = await ViteConfig.fromFile(path); - expect(res.error).to.eql(undefined); - expect(res.module).to.eql({}); - }); - - it('loads main samples', async () => { - const test = async (path: t.StringPath) => { - const res = await Vite.Config.fromFile(path); - expect(res.error).to.eql(undefined); - expect(ViteConfig.Is.paths(res.module.paths)).to.be.true; - }; - - await test('src/-test/vite.sample-config/simple/vite.config.ts'); - await test('src/-test/vite.sample-config/custom/vite.config.ts'); - }); - - it('fail: not found', async () => { - const res = await ViteConfig.fromFile('/foo/404/vite.config.ts'); - expect(res.error?.message).to.include('Module not found at path'); - expect(res.error?.cause?.name).to.eql('TypeError'); - expect(res.module).to.eql({}); - }); - }); }); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/-plugins.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/-plugins.test.ts index 7e63541fe6..3ff172db59 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/-plugins.test.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/-plugins.test.ts @@ -7,10 +7,12 @@ describe('ViteConfig: common plugins', () => { const includes = (name: string) => res.some((p) => p.name === name); expect(includes('vite-plugin-wasm')).to.be.true; expect(includes('vite:react-swc')).to.be.true; + expect(includes('deno')).to.be.true; + expect(includes('deno:prefix')).to.be.true; }); it('none (via options)', async () => { - const res = await commonPlugins({ wasm: false, react: false }); + const res = await commonPlugins({ wasm: false, react: false, deno: false }); expect(res.length).to.eql(0); }); }); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/t.paths.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/t.paths.ts index f64d748e66..9c948f93f3 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/t.paths.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/t.paths.ts @@ -1,9 +1,11 @@ import type { t } from './common.ts'; +/** + * Representation of paths for a Vite configuration. + */ export type ViteConfigPaths = { readonly cwd: t.StringDir; readonly app: t.ViteConfigPathsApp; - readonly lib: t.ViteConfigPathsLib; }; /** @@ -27,9 +29,3 @@ export type ViteConfigPathsApp = { */ readonly base: t.StringDir; }; - -/** - * Paths for "library mode" bundles. - * https://vite.dev/guide/build.html#library-mode - */ -export type ViteConfigPathsLib = {}; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/t.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/t.ts index 1277584547..587e91fba9 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/t.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/t.ts @@ -1,4 +1,3 @@ -import type { defineConfig } from 'vite'; import type { t } from './common.ts'; export type * from './t.app.ts'; @@ -38,7 +37,7 @@ export type ViteConfigLib = { /** * Attempts to dynamically load a `vite.config.ts` module. */ - fromFile(path?: t.StringPath): Promise<ViteConfigFromFile>; + fromFile(configDir?: t.StringDir): Promise<ViteConfigFromFile>; }; /** @@ -56,8 +55,12 @@ export type ViteBundleIO = { in: t.StringDir; out: t.StringDir }; * Common plugins (default: true). */ export type ViteConfigCommonPlugins = { + /** Flag indicating if the official `deno-vite` plugin should be included. */ + deno?: boolean; + /** Flag indicating if the "react+swc" plugin should be included. */ react?: boolean; + /** Flag indicating if the "wasm" plugin should be included. */ wasm?: boolean; }; @@ -74,10 +77,16 @@ export type ViteModuleChunksArgs = { /** * The result from the `Vite.Config.fromFile` method. + * See also: + * https://vite.dev/guide/api-javascript.html#loadconfigfromfile */ export type ViteConfigFromFile = { + /** Flag indicating if the config file exists on the filesystem. */ exists: boolean; - path: t.StringAbsolutePath; - module: { defineConfig?: typeof defineConfig; paths?: t.ViteConfigPaths }; + + /** The paths of the Vite configuration. */ + paths?: t.ViteConfigPaths; + + /** Any error details while loading. */ error?: t.StdError; }; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/u.app.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/u.app.ts index 8cb82e26a8..01effe5268 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/u.app.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/u.app.ts @@ -15,6 +15,7 @@ export const app: t.ViteConfigLib['app'] = async (options = {}) => { const input = Path.join(paths.cwd, paths.app.entry); const outDir = Path.join(paths.cwd, paths.app.outDir); + const publicDir = Path.join(paths.cwd, 'public'); const root = Path.dirname(input); /** @@ -55,6 +56,7 @@ export const app: t.ViteConfigLib['app'] = async (options = {}) => { const res: t.ViteUserConfig = { root, + publicDir, base: paths.app.base, server: { fs: { allow: ['..'] } }, // NB: allows stepping up out of the {cwd} and access other folders in the monorepo. worker: { format }, diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/u.fromFile.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/u.fromFile.ts index 1cb24ed888..d508b03e88 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/u.fromFile.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/u.fromFile.ts @@ -1,56 +1,57 @@ -import { type t, Delete, Err, Path } from './common.ts'; -import { Is } from './m.Is.ts'; - -type R = t.ViteConfigFromFile; +import { loadConfigFromFile } from 'vite'; +import { type t, Delete, Err, Path, PATHS } from './common.ts'; /** * Attempts to dynamically load a `vite.config.ts` module. */ export const fromFile: t.ViteConfigLib['fromFile'] = async (input) => { const errors = Err.errors(); - const path = wrangle.path(input); - const res = await loadModule(path, errors); - return { - path, - exists: res.exists, - module: wrangle.module(res.mod), + const configRoot = wrangle.configDir(input); + + /** + * TODO š· change configRoot to ./.tmp/sample/<vite.config.ts> + */ + + const command = 'build'; + const mode = 'production'; + const fromFile = await loadConfigFromFile( + { command, mode }, // param: configEnv + undefined, // param: configFile + configRoot, // param: configRoot + undefined, // param: logLevel + undefined, // param: customLogger + 'native', // param: configLoader + ); + + const exists = fromFile !== null; + if (!exists) errors.push(`A config file could not be found in directory: ${configRoot}`); + + let paths: t.ViteConfigPaths | undefined; + if (exists) { + paths = { + cwd: Path.dirname(fromFile.path), + app: { + entry: Path.trimCwd(Path.join(fromFile.config.root ?? '', PATHS.html.index)), + outDir: Path.trimCwd(fromFile.config.build?.outDir ?? PATHS.dist), + base: fromFile.config.base ?? PATHS.base, + }, + }; + } + + return Delete.undefined<t.ViteConfigFromFile>({ + exists, + paths, error: errors.toError(), - }; + }); }; /** * Helpers */ -async function loadModule(path: string, errors: t.ErrorCollection) { - let exists = false; - path = `file://${path.replace(/^file\:\/\//, '')}`; - try { - const mod = await import(path); - exists = true; - return { mod, exists }; - } catch (cause: unknown) { - const unexpected = `Unexpected error while importing module from path: ${path}`; - if (!(cause instanceof Error)) { - errors.push(unexpected); - } else { - if (cause.message.includes('Module not found')) { - exists = false; - errors.push(`Module not found at path: ${path}`, cause); - } else { - errors.push(unexpected, cause); - } - } - return { exists }; - } -} - const wrangle = { - path(input?: string): t.StringAbsolutePath { - return typeof input === 'string' ? Path.resolve(input) : Path.resolve('vite.config.ts'); - }, - module(mod: any): R['module'] { - const defineConfig = typeof mod?.default === 'function' ? mod.default : undefined; - const paths = Is.paths(mod?.paths) ? mod.paths : undefined; - return Delete.undefined({ defineConfig, paths }); + configDir(input?: string) { + if (typeof input !== 'string') return Path.cwd(); + if (input.endsWith('vite.config.ts')) return Path.dirname(input); + return input; }, } as const; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/u.paths.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/u.paths.ts index d899b47f4b..2e2a0015a4 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/u.paths.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/u.paths.ts @@ -12,10 +12,9 @@ export const paths: F = (input) => { const app: t.DeepMutable<t.ViteConfigPathsApp> = { entry: PATHS.html.index, - base: './', + base: PATHS.base, outDir: PATHS.dist, }; - const lib: t.DeepMutable<t.ViteConfigPathsLib> = {}; if (valueExists(options.app?.entry)) app.entry = options.app?.entry; if (valueExists(options.app?.base)) app.base = options.app?.base; @@ -25,7 +24,7 @@ export const paths: F = (input) => { app.base = app.base.trim(); app.outDir = app.outDir.trim(); - return { cwd, app, lib }; + return { cwd, app }; }; /** diff --git a/code/sys.driver/driver-vite/src/m.Vite.Config/u.plugins.ts b/code/sys.driver/driver-vite/src/m.Vite.Config/u.plugins.ts index 4efbaec479..7586cd39aa 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Config/u.plugins.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Config/u.plugins.ts @@ -1,22 +1,35 @@ +import deno from '@deno/vite-plugin'; import react from '@vitejs/plugin-react-swc'; import wasm from 'vite-plugin-wasm'; -import { type t } from './common.ts'; + +import type { t } from './common.ts'; export async function commonPlugins(options: t.ViteConfigCommonPlugins = {}) { const plugins: t.VitePluginOption[] = []; - // WASM support. + /** + * The official Denoā¢ļø vite-plugin. + */ + if (options.deno ?? true) { + plugins.push(deno() as t.VitePlugin[]); + } + + /** + * WASM support. + */ if (options.wasm ?? true) { // deno-lint-ignore ban-ts-comment // @ts-ignore plugins.push(wasm()); } - // React (via the SWC compiler). - // - https://github.com/vitejs/vite-plugin-react-swc - // - https://swc.rs + /** + * React (via the SWC compiler). + * - https://github.com/vitejs/vite-plugin-react-swc + * - https://swc.rs + */ if (options.react ?? true) { - plugins.push(react() as any); + plugins.push(react() as t.VitePluginOption[]); } // Finish up. diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-.test.ts new file mode 100644 index 0000000000..512376e882 --- /dev/null +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-.test.ts @@ -0,0 +1,95 @@ +import { type t, SAMPLE, describe, expect, it, pkg } from '../-test.ts'; +import { Vite } from '../mod.ts'; + +describe('Vite: Template Generation', () => { + const testFs = () => SAMPLE.fs('Vite.tmpl'); + + const pathAssertions = (paths: t.StringPath[]) => { + return { + paths, + exists(endsWith: t.StringPath) { + expect(paths.some((p) => p.endsWith(endsWith))).to.eql(true); + }, + } as const; + }; + + it('--tmpl: Default', async () => { + const fs = testFs(); + expect(await fs.ls()).to.eql([]); + + const tmpl = await Vite.Tmpl.create(); + const res = await tmpl.write(fs.dir); + expect(res.ctx).to.eql({ version: pkg.version, tmpl: 'Default' }); + + const a = (await fs.ls()).toSorted(); + const b = (await res.target.ls()).toSorted(); + expect(a).to.eql(b); + + const assert = pathAssertions(a); + [ + '-scripts/-tmp.ts', + '.gitignore', + '.npmrc', + '.vscode/settings.json', + 'README.md', + 'deno.json', + 'imports.json', + 'src/-test.ts', + 'src/-test/-sample/m.Foo.ts', + 'src/-test/-sample/t.ts', + 'src/-test/-sample/ui.Foo.tsx', + 'src/-test/entry.tsx', + 'src/-test/index.html', + 'src/-test/mod.ts', + 'src/.test.ts', + 'src/common.ts', + 'src/common/libs.ts', + 'src/common/mod.ts', + 'src/common/t.ts', + 'src/mod.ts', + 'src/pkg.ts', + 'src/types.ts', + 'vite.config.ts', + ].forEach((path) => assert.exists(path)); + }); + + it('--tmpl: ComponentLib', async () => { + const fs = testFs(); + expect(await fs.ls()).to.eql([]); + + const tmpl = await Vite.Tmpl.create({ tmpl: 'ComponentLib' }); + const res = await tmpl.write(fs.dir); + expect(res.ctx).to.eql({ version: pkg.version, tmpl: 'ComponentLib' }); + + const a = (await fs.ls()).toSorted(); + const b = (await res.target.ls()).toSorted(); + expect(a).to.eql(b); + + const assert = pathAssertions(a); + [ + '-scripts/-tmp.ts', + '.gitignore', + '.npmrc', + '.vscode/settings.json', + 'README.md', + 'deno.json', + 'imports.json', + 'src/-test.ts', + 'src/-test/-sample/m.Foo.ts', + 'src/-test/-sample/t.ts', + 'src/-test/-sample/ui.Foo.tsx', + 'src/-test/entry.tsx', + 'src/-test/index.html', + 'src/-test/mod.ts', + 'src/.test.ts', + 'src/common.ts', + 'src/common/libs.ts', + 'src/common/mod.ts', + 'src/common/t.ts', + 'src/mod.ts', + 'src/pkg.ts', + 'src/types.ts', + 'vite.config.ts', + ].forEach((path) => assert.exists(path)); + }); +}); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-bundle.json b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-bundle.json index 714456a288..e41ec5fe8b 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-bundle.json +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/-bundle.json @@ -1,19 +1,16 @@ { - "-scripts/-tmp.ts": "data:application/typescript;base64,Y29uc29sZS5pbmZvKCfwn5GLJywgaW1wb3J0Lm1ldGEudXJsKTsK", + "-scripts/-tmp.ts": "data:application/typescript;base64,Y29uc29sZS5pbmZvKGBcbvCfkYsgJHtpbXBvcnQubWV0YS51cmx9XG5gKTsK", ".gitignore-": "data:text/plain;base64,bm9kZV9tb2R1bGVzCmRpc3QKLnRtcAouc3djCgouc3lzCi52c2NvZGUKCi1iYWNrdXAKLWJhY2t1cC8qKgo=", ".npmrc": "data:text/plain;base64,IyBEb2NzOiBodHRwczovL2pzci5pby9kb2NzL25wbS1jb21wYXRpYmlsaXR5I2luc3RhbGxpbmctYW5kLXVzaW5nLWpzci1wYWNrYWdlcwpAanNyOnJlZ2lzdHJ5PWh0dHBzOi8vbnBtLmpzci5pbwo=", - ".sys/deps.sys.yaml": "data:text/plain;base64,ZGVuby5qc29uOgogIC0gaW1wb3J0OiBqc3I6QHN5cy90eXBlc0AwLjAuODIKICAtIGltcG9ydDoganNyOkBzeXMvc3RkQDAuMC4xMzEKICAtIGltcG9ydDoganNyOkBzeXMvY29sb3JAMC4wLjM1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3Rlc3RpbmdAMC4wLjc1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2ZzQDAuMC43OQogIC0gaW1wb3J0OiBqc3I6QHN5cy9jbGlAMC4wLjY0CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3Byb2Nlc3NAMC4wLjY1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2NyeXB0b0AwLjAuNjQKICAtIGltcG9ydDoganNyOkBzeXMvaHR0cEAwLjAuNDYKICAtIGltcG9ydDoganNyOkBzeXMvdGV4dEAwLjAuNzMKICAtIGltcG9ydDoganNyOkBzeXMvdG1wbEAwLjAuNzkKICAtIGltcG9ydDoganNyOkBzeXMvY21kQDAuMC44MAogIC0gaW1wb3J0OiBqc3I6QHN5cy9qc3JAMC4wLjQ1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3VpLWNzc0AwLjAuNzAKICAtIGltcG9ydDoganNyOkBzeXMvdWktZG9tQDAuMC43NwogIC0gaW1wb3J0OiBqc3I6QHN5cy91aS1yZWFjdEAwLjAuODQKICAtIGltcG9ydDoganNyOkBzeXMvdWktcmVhY3QtZGV2aGFybmVzc0AwLjAuODIKICAtIGltcG9ydDoganNyOkBzeXMvdWktcmVhY3QtY29tcG9uZW50c0AwLjAuMzgKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLWF1dG9tZXJnZUAwLjAuODQKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLWRlbm9AMC4wLjg2CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2RyaXZlci1pbW1lckAwLjAuODYKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLW9ic2lkaWFuQDAuMC43MwogIC0gaW1wb3J0OiBqc3I6QHN5cy9kcml2ZXItb2xsYW1hQDAuMC40MAogIC0gaW1wb3J0OiBqc3I6QHN5cy9kcml2ZXItb3JiaXRlckAwLjAuNjMKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLXF1aWxpYnJpdW1AMC4wLjc2CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2RyaXZlci12aXRlQDAuMC4xMjEKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLXZpdGVwcmVzc0AwLjAuMjg0LWFscGhhLjIKICAtIGltcG9ydDoganNyOkBzeXMvc3lzQDAuMC41NQogIC0gaW1wb3J0OiBqc3I6QHN5cy9tYWluQDAuMC41NwogIC0gaW1wb3J0OiBqc3I6QHRkYi9hcGlAMC4wLjcyCiAgLSBpbXBvcnQ6IGpzcjpAdGRiL3NsY0AwLjAuNjgKICAtIGltcG9ydDoganNyOkB0ZGIvdG1wQDAuMC43MgogIC0gaW1wb3J0OiBqc3I6QHN5cy9uYW1lQDAuMC4wCiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3RtcEAwLjAuOTUKcGFja2FnZS5qc29uOiBbXQo=", - ".sys/deps.yaml": "data:text/plain;base64,IwojICBTeXN0ZW0gRGVwZW5kZW5jaWVzICgiaW1wb3J0cyIpCiMKIyAgICAgICAgLi/wn5KmCiMgICAgICAgICAgfCAgICAgICAgICAgIGRlbm8uanNvbgojICAgICAgICAgIHwod3JpdGUpICDihpIgIGRlbm8uaW1wb3J0cy5qc29uCiMgICAgICAgICAgfCh3cml0ZSkgIOKGkiAgcGFja2FnZS5qc29uCiMKIyAgVGhpcyBpcyB0aGUgInNpbmdsZS1zb3VyY2Utb2YtdHJ1dGgiIHdpdGggcmVnYXJkcyB0byBkZXBlbmRlbmNpZXMgYW5kIHZlcnNpb25pbmcuCiMgIEltcG9ydCBtYXBzIChpbiB0aGUgYGRlbm8uanNvbmAgYW5kIGBwYWNrYWdlLmpzb25gIGZpbGVzKSBhcmUgYXV0by1nZW5lcmF0ZWQKIyAgZnJvbSB0aGlzIGNvbmZpZyBkZWZpbml0aW9uLgojCiMgIEFsc28sIGFzIGEgcHJvZ3JhbW1hdGljIEFQSSwgb3RoZXIgZG93bnN0cmVhbSBkZXBlbmRlbmNpZXMKIyAgKHN1Y2ggYXMgdGVtcGxhdGUgZ2VuZXJhdG9ycywgc2VlIGBAc3lzL3RtcGxgKSB1c2UgdGhpcyBkZWZpbml0aW9uCiMgIGZpbGUgdG8gY2FsY3VsYXRlIHRoZSAibGF0ZXN0IiB2ZXJzaW9ucyB0byBpbmplY3QgaW50bywgc2F5LAojICBhIGBwYWNrYWdlLmpzb25gIGZpbGUgZm9yIGEgc2NhZmZvbGRlZCBwcm9qZWN0LgojCgpncm91cHM6CiAgc3RkL2Rlbm86CiAgICAjIERlbm8gc3RhbmRhcmQgbGlicyAoInN0ZCIpLgogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL2FzeW5jQDEuMC4xMAogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL2RhdGV0aW1lQDAuMjI1LjMKICAgIC0gaW1wb3J0OiBqc3I6QHN0ZC9kb3RlbnZAMC4yMjUuMwogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL2VuY29kaW5nQDEuMC43CiAgICAtIGltcG9ydDoganNyOkBzdGQvZnNAMS4wLjEzCiAgICAtIGltcG9ydDoganNyOkBzdGQvcGF0aEAxLjAuOAogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL3NlbXZlckAxLjAuNAogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL3Rlc3RpbmdAMS4wLjkKICAgIC0gaW1wb3J0OiBqc3I6QHN0ZC91dWlkQDEuMC40CgogIGF1dG9tZXJnZToKICAgICMgaHR0cHM6Ly9hdXRvbWVyZ2Uub3JnCiAgICAtIGltcG9ydDogbnBtOkBhdXRvbWVyZ2UvYXV0b21lcmdlQDIuMi44CiAgICAtIGltcG9ydDogbnBtOkBhdXRvbWVyZ2UvYXV0b21lcmdlLXJlcG9AMS4yLjEKICAgIC0gaW1wb3J0OiBucG06QGF1dG9tZXJnZS9hdXRvbWVyZ2UtcmVwby1uZXR3b3JrLWJyb2FkY2FzdGNoYW5uZWxAMS4yLjEKICAgIC0gaW1wb3J0OiBucG06QGF1dG9tZXJnZS9hdXRvbWVyZ2UtcmVwby1zdG9yYWdlLWluZGV4ZWRkYkAxLjIuMQogICAgLSBpbXBvcnQ6IG5wbTpAYXV0b21lcmdlL2F1dG9tZXJnZS1yZXBvLXN0b3JhZ2Utbm9kZWZzQDEuMi4xCiAgICAtIGltcG9ydDogbnBtOkBvbnNldHNvZnR3YXJlL2F1dG9tZXJnZS1wYXRjaGVyQDAuMTQuMAoKICBjcnlwdG86CiAgICAtIGltcG9ydDogbnBtOkBub2JsZS9oYXNoZXNAMS43LjEKICAgICAgd2lsZGNhcmQ6IHRydWUKCiAgYnVpbGQvdG9vbHM6CiAgICAtIGltcG9ydDogbnBtOkB2aXRlanMvcGx1Z2luLXJlYWN0LXN3Y0AzLjguMAogICAgLSBpbXBvcnQ6IG5wbTpyb2xsdXBANC4zNC44CiAgICAtIGltcG9ydDogbnBtOnZpdGVANi4xLjEKICAgIC0gaW1wb3J0OiBucG06dml0ZS1wbHVnaW4td2FzbUAzLjQuMQoKICB1aS9yZWFjdDoKICAgIC0gaW1wb3J0OiBucG06QHR5cGVzL3JlYWN0QDE4LjMuMTgKICAgIC0gaW1wb3J0OiBucG06QHR5cGVzL3JlYWN0LWRvbUAxOC4zLjUKICAgIC0gaW1wb3J0OiBucG06cmVhY3RAMTguMy4xCiAgICAtIGltcG9ydDogbnBtOnJlYWN0LWRvbUAxOC4zLjEKCmRlbm8uanNvbjoKICAtIGdyb3VwOiBzdGQvZGVubwogIC0gZ3JvdXA6IGNyeXB0bwogIC0gZ3JvdXA6IGF1dG9tZXJnZQoKICAjIENMSSB0b29scwogIC0gaW1wb3J0OiBqc3I6QGNsaWZmeS9rZXlwcmVzc0AxLjAuMC1yYy43CiAgLSBpbXBvcnQ6IGpzcjpAY2xpZmZ5L3Byb21wdEAxLjAuMC1yYy43CiAgLSBpbXBvcnQ6IGpzcjpAY2xpZmZ5L3RhYmxlQDEuMC4wLXJjLjcKCiAgIyBTdW5kcnk6IE5QTQogIC0gaW1wb3J0OiBucG06QHR5cGVzL2RpZmZANy4wLjEKICAtIGltcG9ydDogbnBtOmNoYWlANQogIC0gaW1wb3J0OiBucG06YXBwcm94LXN0cmluZy1tYXRjaEAyCiAgLSBpbXBvcnQ6IG5wbTpkYXRlLWZuc0A0CiAgLSBpbXBvcnQ6IG5wbTpzdWJob3N0aW5nQDAuMS4wLWFscGhhLjEKICAtIGltcG9ydDogbnBtOmRpZmZANwogIC0gaW1wb3J0OiBucG06ZmFzdC1qc29uLXBhdGNoQDMuMS4xCiAgLSBpbXBvcnQ6IG5wbTpmYWtlLWluZGV4ZWRkYkA2LjAuMAogIC0gaW1wb3J0OiBucG06aGFwcHktZG9tQDE3LjEuMwogIC0gaW1wb3J0OiBucG06aGFzaC1pdEA2LjAuMAogIC0gaW1wb3J0OiBucG06aWdub3JlQDcKICAtIGltcG9ydDogbnBtOmltbWVyQDEwCiAgLSBpbXBvcnQ6IG5wbTpvcmFAOC4yLjAKICAtIGltcG9ydDogbnBtOm9sbGFtYUAwLjUuMTMKICAtIGltcG9ydDogbnBtOnByZXR0eS1ieXRlc0A2LjEuMQogIC0gaW1wb3J0OiBucG06cmFtZGFAMC4zMC4xCiAgLSBpbXBvcnQ6IG5wbTpyYW1iZGFAOS40LjIKICAtIGltcG9ydDogbnBtOnJ4anNANy44LjIKICAtIGltcG9ydDogbnBtOnN0cmlwLWFuc2lANwogIC0gaW1wb3J0OiBucG06c3ViaG9zdGluZ0AwLjEuMC1hbHBoYS4xCiAgLSBpbXBvcnQ6IG5wbTp0aW55Y29sb3IyQDEuNi4wCiAgLSBpbXBvcnQ6IG5wbTp0cy1lc3NlbnRpYWxzQDEwLjAuNAogIC0gaW1wb3J0OiBucG06dmFsaWJvdEAxLjAuMC1yYy4xCiAgLSBpbXBvcnQ6IG5wbTp5YW1sQDIuNy4wCgogICMgQnJvd3NlcgogIC0gaW1wb3J0OiBucG06Y3NzdHlwZUAzCiAgLSBpbXBvcnQ6IG5wbTp1YS1wYXJzZXItanNAMi4wLjIKCiAgIyBVSQogIC0gaW1wb3J0OiBucG06cmVhY3QtZXJyb3ItYm91bmRhcnlANQogIC0gaW1wb3J0OiBucG06cmVhY3QtaW5zcGVjdG9yQDYKICAtIGltcG9ydDogbnBtOnJlYWN0LXNwaW5uZXJzQDAuMTUuMAoKcGFja2FnZS5qc29uOgogIC0gZ3JvdXA6IHN0ZC9kZW5vCiAgLSBncm91cDogY3J5cHRvCiAgLSBncm91cDogYnVpbGQvdG9vbHMKICAgIGRldjogdHJ1ZQoKICAtIGltcG9ydDogbnBtOmhvbm9ANC43LjIKCiAgIyBVSQogIC0gZ3JvdXA6IHVpL3JlYWN0CiAgLSBpbXBvcnQ6IG5wbTpyZWFjdC1pY29uc0A1LjUuMAogIC0gaW1wb3J0OiBucG06QHZpZHN0YWNrL3JlYWN0QDEuMTIuMTIKCiAgIyBVSTpGcmFtZXdvcmtzCiAgLSBpbXBvcnQ6IG5wbTp2aXRlcHJlc3NAMS42LjMKICAtIGltcG9ydDogbnBtOnZ1ZUAzLjUuMTMK", ".vscode/settings.json": "data:application/json;base64,ewogICJkZW5vLmVuYWJsZSI6IHRydWUKfQo=", "README.md": "data:text/markdown;base64,IyBNb2R1bGUKCg==", - "deno.json": "data:application/json;base64,ewogICJ2ZXJzaW9uIjogIjAuMC4wIiwKICAibGljZW5zZSI6ICJNSVQiLAogICJ0YXNrcyI6IHsKICAgICJkZXYiOiAiZGVubyAgICAgIHJ1biAtUldORSAtLWFsbG93LXJ1biAtLWFsbG93LWZmaSA8RU5UUlk+IC0tY21kPWRldiAgICAtLWluPS4vc3JjLy10ZXN0L2luZGV4Lmh0bWwiLAogICAgImJ1aWxkIjogImRlbm8gICAgcnVuIC1SV0UgIC0tYWxsb3ctcnVuIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9YnVpbGQgIC0taW49Li9zcmMvLXRlc3QvaW5kZXguaHRtbCIsCiAgICAic2VydmUiOiAiZGVubyAgICBydW4gLVJORSAgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgPEVOVFJZPiAtLWNtZD1zZXJ2ZSIsCgogICAgImNsZWFuIjogImRlbm8gICAgcnVuIC1SV0UgIC0tYWxsb3ctZmZpICAgICAgICAgICAgIDxFTlRSWT4gLS1jbWQ9Y2xlYW4iLAogICAgInVwZ3JhZGUiOiAiZGVubyAgcnVuIC1SV05FIC0tYWxsb3ctcnVuIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9dXBncmFkZSIsCiAgICAiYmFja3VwIjogImRlbm8gICBydW4gLVJXRSAgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgPEVOVFJZPiAtLWNtZD1iYWNrdXAiLAogICAgImhlbHAiOiAiZGVubyAgICAgcnVuIC1SRSAgIC0tYWxsb3ctZmZpICAgICAgICAgICAgIDxFTlRSWT4gLS1jbWQ9aGVscCIsCgogICAgInN5cyI6ICJkZW5vICAgICAgcnVuIC1SV05FIDxFTlRSWV9TWVM+IiwKICAgICJ0bXAiOiAiZGVubyAgICAgIHJ1biAtQSAuLy1zY3JpcHRzLy10bXAudHMiCiAgfSwKICAiY29tcGlsZXJPcHRpb25zIjogewogICAgInN0cmljdCI6IHRydWUsCiAgICAibGliIjogWyJkZW5vLm5zIiwgImVzbmV4dCIsICJkb20iLCAiZG9tLml0ZXJhYmxlIiwgImRvbS5hc3luY2l0ZXJhYmxlIl0sCiAgICAidHlwZXMiOiBbInZpdGUvY2xpZW50IiwgIkB0eXBlcy9yZWFjdCJdLAogICAgImpzeCI6ICJyZWFjdCIsCiAgICAianN4RmFjdG9yeSI6ICJSZWFjdC5jcmVhdGVFbGVtZW50IiwKICAgICJqc3hGcmFnbWVudEZhY3RvcnkiOiAiUmVhY3QuRnJhZ21lbnQiCiAgfSwKICAid29ya3NwYWNlIjogW10sCiAgIm5vZGVNb2R1bGVzRGlyIjogImF1dG8iLAogICJpbXBvcnRzIjogewogICAgIjxTRUxGX0lNUE9SVF9OQU1FPiI6ICI8U0VMRl9JTVBPUlRfVVJJPiIKICB9Cn0K", - "package.json": "data:application/json;base64,ewogICJkZXBlbmRlbmNpZXMiOiB7CiAgICAiQHN5cy9zdGQiOiAibnBtOkBqc3Ivc3lzX19zdGQiLAogICAgIkBzeXMvdG1wIjogIm5wbTpAanNyL3N5c19fdG1wIiwKICAgICJAc3lzL3R5cGVzIjogIm5wbTpAanNyL3N5c19fdHlwZXMiLAogICAgIkBzeXMvdWktY3NzIjogIm5wbTpAanNyL3N5c19fdWktY3NzIiwKICAgICJyZWFjdCI6ICIiLAogICAgInJlYWN0LWRvbSI6ICIiLAogICAgInZhbGlib3QiOiAiIgogIH0sCiAgImRldkRlcGVuZGVuY2llcyI6IHsKICAgICJAdHlwZXMvcmVhY3QiOiAiIiwKICAgICJAdHlwZXMvcmVhY3QtZG9tIjogIiIsCiAgICAiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djIjogIiIsCiAgICAicm9sbHVwIjogIiIsCiAgICAidml0ZSI6ICIiLAogICAgInZpdGUtcGx1Z2luLXdhc20iOiAiIgogIH0KfQo=", + "deno.json": "data:application/json;base64,ewogICJ2ZXJzaW9uIjogIjAuMC4wIiwKICAidGFza3MiOiB7CiAgICAiZGV2IjogImRlbm8gICAgICBydW4gLVJXTkUgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgPEVOVFJZPiAtLWNtZD1kZXYgICAgLS1pbj0uL3NyYy8tdGVzdC9pbmRleC5odG1sIiwKICAgICJidWlsZCI6ICJkZW5vICAgIHJ1biAtUldFICAtLWFsbG93LXJ1biAtLWFsbG93LWZmaSA8RU5UUlk+IC0tY21kPWJ1aWxkICAtLWluPS4vc3JjLy10ZXN0L2luZGV4Lmh0bWwiLAogICAgInNlcnZlIjogImRlbm8gICAgcnVuIC1STkUgIC0tYWxsb3ctcnVuIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9c2VydmUiLAoKICAgICJ0ZXN0IjogImRlbm8gICAgIHRlc3QgLVJXTkUgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgLS1hbGxvdy1zeXMiLAogICAgImNsZWFuIjogImRlbm8gICAgcnVuIC1SV0UgIC0tYWxsb3ctZmZpICAgICAgICAgICAgIDxFTlRSWT4gLS1jbWQ9Y2xlYW4iLAogICAgInVwZ3JhZGUiOiAiZGVubyAgcnVuIC1SV05FIC0tYWxsb3ctcnVuIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9dXBncmFkZSIsCiAgICAiYmFja3VwIjogImRlbm8gICBydW4gLVJXRSAgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgPEVOVFJZPiAtLWNtZD1iYWNrdXAiLAogICAgImhlbHAiOiAiZGVubyAgICAgcnVuIC1SRSAgIC0tYWxsb3ctZmZpICAgICAgICAgICAgIDxFTlRSWT4gLS1jbWQ9aGVscCIsCgogICAgInRtcCI6ICJkZW5vICAgICAgcnVuIC1BIC4vLXNjcmlwdHMvLXRtcC50cyIKICB9LAogICJjb21waWxlck9wdGlvbnMiOiB7CiAgICAic3RyaWN0IjogdHJ1ZSwKICAgICJsaWIiOiBbImRlbm8ubnMiLCAiZXNuZXh0IiwgImRvbSIsICJkb20uaXRlcmFibGUiLCAiZG9tLmFzeW5jaXRlcmFibGUiXSwKICAgICJ0eXBlcyI6IFsidml0ZS9jbGllbnQiLCAiQHR5cGVzL3JlYWN0Il0sCiAgICAianN4IjogInJlYWN0LWpzeCIsCiAgICAianN4SW1wb3J0U291cmNlIjogInJlYWN0IgogIH0sCiAgIndvcmtzcGFjZSI6IFtdLAogICJpbXBvcnRNYXAiOiAiLi9pbXBvcnRzLmpzb24iLAogICJub2RlTW9kdWxlc0RpciI6ICJhdXRvIiwKICAibGljZW5zZSI6ICJNSVQiCn0K", + "imports.json": "data:application/json;base64,ewogICJpbXBvcnRzIjogewogICAgIkBzeXMvc3RkIjogImpzcjpAc3lzL3N0ZEAwLjAuMTQxIiwKICAgICJAc3lzL3RtcCI6ICJqc3I6QHN5cy90bXBAMC4wLjExMCIsCiAgICAiQHN5cy90eXBlcyI6ICJqc3I6QHN5cy90eXBlc0AwLjAuOTIiLAogICAgIkBzeXMvdWktY3NzIjogImpzcjpAc3lzL3VpLWNzc0AwLjAuODEiLAogICAgIkBzeXMvZHJpdmVyLXZpdGUiOiAianNyOkBzeXMvZHJpdmVyLXZpdGVAMC4wLjEzNyIsCiAgICAicmVhY3QiOiAibnBtOnJlYWN0QDE4LjMuMSIsCiAgICAicmVhY3QtZG9tIjogIm5wbTpyZWFjdC1kb21AMTguMy4xIiwKICAgICJAdHlwZXMvcmVhY3QiOiAibnBtOkB0eXBlcy9yZWFjdEAxOC4zLjE4IiwKICAgICJAdHlwZXMvcmVhY3QtZG9tIjogIm5wbTpAdHlwZXMvcmVhY3QtZG9tQDE4LjMuNSIsCiAgICAiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djIjogIm5wbTpAdml0ZWpzL3BsdWdpbi1yZWFjdC1zd2NAMy44LjAiLAogICAgInJvbGx1cCI6ICJucG06cm9sbHVwQDQuMzQuOCIsCiAgICAidml0ZSI6ICJucG06dml0ZUA2LjEuMSIsCiAgICAidml0ZS1wbHVnaW4td2FzbSI6ICJucG06dml0ZS1wbHVnaW4td2FzbUAzLjQuMSIKICB9Cn0K", "src/-test.ts": "data:application/typescript;base64,ZXhwb3J0ICogZnJvbSAnLi8tdGVzdC9tb2QudHMnOwo=", "src/-test/-sample/m.Foo.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgKiBhcyB0IGZyb20gJy4vdC50cyc7CgovKioKICogQG1vZHVsZQogKiBTYW1wbGUgRm9vIG1vZHVsZS4KICovCmV4cG9ydCBjb25zdCBGb286IHQuRm9vID0geyBtc2c6ICfwn5GLJyB9Owo=", "src/-test/-sample/t.ts": "data:application/typescript;base64,ZXhwb3J0IHR5cGUgRm9vID0geyBtc2c/OiBzdHJpbmcgfTsK", - "src/-test/-sample/ui.Foo.tsx": "data:application/typescript+jsx;base64,aW1wb3J0IHsgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCc7CgppbXBvcnQgeyBGb28gfSBmcm9tICdAc3lzL3RtcC91aSc7CmltcG9ydCB7IENvbG9yLCBjc3MgfSBmcm9tICdAc3lzL3VpLWNzcyc7CmltcG9ydCB0eXBlIHsgdCB9IGZyb20gJy4uLy4uL2NvbW1vbi50cyc7CgovKioKICogU2FtcGxlIENvbXBvbmVudCBkZW1vbnN0cmF0aW5nIHRoZSBmdW5kYW1lbnRhbHMgb2YgUmVhY3QKICogYW5kIHByb3ZpbmcgbW9kdWxlIGltcG9ydGluZyB3b3JrcyBhY3Jvc3MgdGhlIG1vbm9yZXBvLgogKgogKiAgIC0gTW9kdWxlICJpbXBvcnRzIiAocHJvdmUgQHN5cyBpbXBvcnRzIGZyb20gdGhlIHdvcmtzcGFjZSB3b3JrKQogKiAgIC0gU3R5bGU6IENTUyBwcmltaXRpdmVzCiAqICAgLSBTdHlsZTogQ29sb3IgcHJpbWl0aXZlcwogKgogKi8KZXhwb3J0IHR5cGUgRm9vQ29tcG9uZW50ID0gewogIHRoZW1lPzogdC5Db21tb25UaGVtZTsKICBzdHlsZT86IHQuQ3NzSW5wdXQ7Cn07CgpleHBvcnQgY29uc3QgRm9vU2FtcGxlOiBSZWFjdC5GQzxGb29Db21wb25lbnQ+ID0gKHByb3BzKSA9PiB7CiAgY29uc3QgW2lzT3Zlciwgc2V0T3Zlcl0gPSB1c2VTdGF0ZShmYWxzZSk7CiAgY29uc3Qgb3ZlciA9IChpc092ZXI6IGJvb2xlYW4pID0+ICgpID0+IHNldE92ZXIoaXNPdmVyKTsKCiAgY29uc3QgdGhlbWUgPSBDb2xvci50aGVtZShwcm9wcy50aGVtZSA/PyAnRGFyaycpOwoKICBjb25zdCBzdHlsZXMgPSB7CiAgICBiYXNlOiBjc3MoewogICAgICBNYXJnaW46IDIwLAogICAgICBwYWRkaW5nOiAyMCwKICAgICAgYmFja2dyb3VuZENvbG9yOiBpc092ZXIgPyAnaG90cGluaycgOiAnbGlnaHRncmVlbicsCiAgICAgIGZvbnRGYW1pbHk6ICdtb25vc3BhY2UnLAogICAgICBjb2xvcjogJ2JsdWUnLAogICAgfSksCiAgICB0aXRsZTogY3NzKHsKICAgICAgYmFja2dyb3VuZENvbG9yOiBDb2xvci5SVUJZLAogICAgICBmb250U2l6ZTogMzAsCiAgICAgIE1hcmdpblk6IDUsCiAgICAgIFBhZGRpbmdYOiAzMCwKICAgICAgUGFkZGluZ1k6IFszMCwgMTVdLAogICAgICBjb2xvcjogJ3JlZCcsCiAgICAgICc6aG92ZXInOiB7IGNvbG9yOiAnZ3JlZW4nIH0sCiAgICB9KSwKICAgIHRoZW1lU2FtcGxlOiBjc3MoewogICAgICBtYXJnaW5Ub3A6IDIwLAogICAgICBkaXNwbGF5OiAnZ3JpZCcsCiAgICAgIHBsYWNlSXRlbXM6ICdjZW50ZXInLAogICAgICBtaW5IZWlnaHQ6IDMwMCwKICAgICAgY29sb3I6IENvbG9yLmFscGhhKHRoZW1lLmZnLCBpc092ZXIgPyAxIDogMC4zKSwKICAgICAgYmFja2dyb3VuZENvbG9yOiB0aGVtZS5iZywKICAgICAgdHJhbnNpdGlvbjogYGNvbG9yIDIwMG1zYCwKICAgIH0pLAogIH07CgogIGNvbnN0IGVsVGhlbWVTYW1wbGUgPSAoCiAgICA8ZGl2IGNsYXNzTmFtZT17c3R5bGVzLnRoZW1lU2FtcGxlLmNsYXNzfT4KICAgICAgPGRpdj5IZWxsbzwvZGl2PgogICAgPC9kaXY+CiAgKTsKCiAgcmV0dXJuICgKICAgIDxkaXYKICAgICAgY2xhc3NOYW1lPXtjc3Moc3R5bGVzLmJhc2UsIHByb3BzLnN0eWxlKS5jbGFzc30KICAgICAgb25Nb3VzZUVudGVyPXtvdmVyKHRydWUpfQogICAgICBvbk1vdXNlTGVhdmU9e292ZXIoZmFsc2UpfQogICAgPgogICAgICA8ZGl2IGNsYXNzTmFtZT17c3R5bGVzLnRpdGxlLmNsYXNzfT4KICAgICAgICA8ZGl2PntgSGVsbG8gV29ybGQg8J+Ri2B9PC9kaXY+CiAgICAgIDwvZGl2PgogICAgICA8ZGl2PntgKHNlZSBjb25zb2xlIGZvciBpbXBvcnQgc2FtcGxlcylgfTwvZGl2PgogICAgICA8ZGl2IHN0eWxlPXt7IHBhZGRpbmdUb3A6IDEwIH19PgogICAgICAgIHsnSW1wb3J0ZWQgZnJvbSDihpAgJ30KICAgICAgICA8Rm9vIC8+CiAgICAgICAgPGRpdj57J/CfkLfwn5C3IFRNUCDwn5C38J+QtyBpbXBvcnQgV0lQJ308L2Rpdj4KICAgICAgICA8Y29kZT57JzxKU1g+IOKGkiBWaXRlIOKGkiBFU00uanMuZC50cyDihpIgbW9kLnRzIOKGkiBKU1Ig4oaSIGltcG9ydCd9PC9jb2RlPgogICAgICA8L2Rpdj4KICAgICAge2VsVGhlbWVTYW1wbGV9CiAgICA8L2Rpdj4KICApOwp9Owo=", - "src/-test/entry.lib.ts": "data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGZuID0gYXN5bmMgKCkgPT4gewogIGNvbnN0IHsgRm9vIH0gPSBhd2FpdCBpbXBvcnQoJy4vLXNhbXBsZS9tLkZvby50cycpOwogIGNvbnN0IHsgRm9vU2FtcGxlIH0gPSBhd2FpdCBpbXBvcnQoJy4vLXNhbXBsZS91aS5Gb28udHN4Jyk7CiAgcmV0dXJuIHsKICAgIEZvbywKICAgIEZvb1NhbXBsZSwKICB9Owp9Owo=", - "src/-test/entry.tsx": "data:application/typescript+jsx;base64,aW1wb3J0IHsgcGtnIH0gZnJvbSAnLi4vY29tbW9uLnRzJzsKCi8qKgogKiBSZW5kZXIgVUkuCiAqLwpnbG9iYWxUaGlzLmRvY3VtZW50LnRpdGxlID0gcGtnLm5hbWU7CmNvbnNvbGUuaW5mbygn8J+QtyAuL2VudHJ5LnRzeCDihpIgUGtnOvCfkqYnLCBwa2cpOwoKaW1wb3J0IHsgU3RyaWN0TW9kZSB9IGZyb20gJ3JlYWN0JzsKaW1wb3J0IHsgY3JlYXRlUm9vdCB9IGZyb20gJ3JlYWN0LWRvbS9jbGllbnQnOwppbXBvcnQgeyBGb29TYW1wbGUgfSBmcm9tICcuLy1zYW1wbGUvdWkuRm9vLnRzeCc7CgovKioKICog8J+QtyBUZXN0ICIgQHN5cyAiIG1vZHVsZSBpbXBvcnRzIGZyb20gYWNyb3NzIHRoZQogKiAgICBuYW1lc3BhY2UgKG1vbm9yZXBvL3dvcmtzcGFjZSkuCiAqLwppbXBvcnQgJ0BzeXMvdG1wL3NhbXBsZS1pbXBvcnRzJzsKCi8qKgogKiBTYW1wbGU6IHJlbmRlciByZWFjdCBjb21wb25lbnQuCiAqLwpjb25zdCByb290ID0gY3JlYXRlUm9vdChkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncm9vdCcpISk7CnJvb3QucmVuZGVyKAogIDxTdHJpY3RNb2RlPgogICAgPEZvb1NhbXBsZSBzdHlsZT17eyBib3JkZXI6IGBzb2xpZCAxcHggYmx1ZWAgfX0gLz4KICA8L1N0cmljdE1vZGU+LAopOwo=", + "src/-test/-sample/ui.Foo.tsx": "data:application/typescript+jsx;base64,aW1wb3J0IHsgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCc7CgovLyBpbXBvcnQgeyBGb28gfSBmcm9tICdAc3lzL3RtcC91aSc7CmltcG9ydCB7IENvbG9yLCBjc3MgfSBmcm9tICdAc3lzL3VpLWNzcyc7CmltcG9ydCB0eXBlIHsgdCB9IGZyb20gJy4uLy4uL2NvbW1vbi50cyc7CgovKioKICogU2FtcGxlIENvbXBvbmVudCBkZW1vbnN0cmF0aW5nIHRoZSBmdW5kYW1lbnRhbHMgb2YgUmVhY3QKICogYW5kIHByb3ZpbmcgbW9kdWxlIGltcG9ydGluZyB3b3JrcyBhY3Jvc3MgdGhlIG1vbm9yZXBvLgogKgogKiAgIC0gTW9kdWxlICJpbXBvcnRzIiAocHJvdmUgQHN5cyBpbXBvcnRzIGZyb20gdGhlIHdvcmtzcGFjZSB3b3JrKQogKiAgIC0gU3R5bGU6IENTUyBwcmltaXRpdmVzCiAqICAgLSBTdHlsZTogQ29sb3IgcHJpbWl0aXZlcwogKgogKi8KZXhwb3J0IHR5cGUgRm9vQ29tcG9uZW50ID0gewogIHRoZW1lPzogdC5Db21tb25UaGVtZTsKICBzdHlsZT86IHQuQ3NzSW5wdXQ7Cn07CgpleHBvcnQgY29uc3QgRm9vU2FtcGxlOiBSZWFjdC5GQzxGb29Db21wb25lbnQ+ID0gKHByb3BzKSA9PiB7CiAgY29uc3QgW2lzT3Zlciwgc2V0T3Zlcl0gPSB1c2VTdGF0ZShmYWxzZSk7CiAgY29uc3Qgb3ZlciA9IChpc092ZXI6IGJvb2xlYW4pID0+ICgpID0+IHNldE92ZXIoaXNPdmVyKTsKCiAgY29uc3QgdGhlbWUgPSBDb2xvci50aGVtZShwcm9wcy50aGVtZSA/PyAnRGFyaycpOwoKICBjb25zdCBzdHlsZXMgPSB7CiAgICBiYXNlOiBjc3MoewogICAgICBNYXJnaW46IDIwLAogICAgICBwYWRkaW5nOiAyMCwKICAgICAgYmFja2dyb3VuZENvbG9yOiBpc092ZXIgPyAnaG90cGluaycgOiAnbGlnaHRncmVlbicsCiAgICAgIGZvbnRGYW1pbHk6ICdtb25vc3BhY2UnLAogICAgICBjb2xvcjogJ2JsdWUnLAogICAgfSksCiAgICB0aXRsZTogY3NzKHsKICAgICAgYmFja2dyb3VuZENvbG9yOiBDb2xvci5SVUJZLAogICAgICBmb250U2l6ZTogMzAsCiAgICAgIE1hcmdpblk6IDUsCiAgICAgIFBhZGRpbmdYOiAzMCwKICAgICAgUGFkZGluZ1k6IFszMCwgMTVdLAogICAgICBjb2xvcjogJ3JlZCcsCiAgICAgICc6aG92ZXInOiB7IGNvbG9yOiAnZ3JlZW4nIH0sCiAgICB9KSwKICAgIHRoZW1lU2FtcGxlOiBjc3MoewogICAgICBtYXJnaW5Ub3A6IDIwLAogICAgICBkaXNwbGF5OiAnZ3JpZCcsCiAgICAgIHBsYWNlSXRlbXM6ICdjZW50ZXInLAogICAgICBtaW5IZWlnaHQ6IDMwMCwKICAgICAgY29sb3I6IENvbG9yLmFscGhhKHRoZW1lLmZnLCBpc092ZXIgPyAxIDogMC4zKSwKICAgICAgYmFja2dyb3VuZENvbG9yOiB0aGVtZS5iZywKICAgICAgdHJhbnNpdGlvbjogYGNvbG9yIDIwMG1zYCwKICAgIH0pLAogIH07CgogIGNvbnN0IGVsVGhlbWVTYW1wbGUgPSAoCiAgICA8ZGl2IGNsYXNzTmFtZT17c3R5bGVzLnRoZW1lU2FtcGxlLmNsYXNzfT4KICAgICAgPGRpdj5IZWxsbzwvZGl2PgogICAgPC9kaXY+CiAgKTsKCiAgcmV0dXJuICgKICAgIDxkaXYKICAgICAgY2xhc3NOYW1lPXtjc3Moc3R5bGVzLmJhc2UsIHByb3BzLnN0eWxlKS5jbGFzc30KICAgICAgb25Nb3VzZUVudGVyPXtvdmVyKHRydWUpfQogICAgICBvbk1vdXNlTGVhdmU9e292ZXIoZmFsc2UpfQogICAgPgogICAgICA8ZGl2IGNsYXNzTmFtZT17c3R5bGVzLnRpdGxlLmNsYXNzfT4KICAgICAgICA8ZGl2PntgSGVsbG8gV29ybGQg8J+Ri2B9PC9kaXY+CiAgICAgIDwvZGl2PgogICAgICA8ZGl2PntgKHNlZSBjb25zb2xlIGZvciBpbXBvcnQgc2FtcGxlcylgfTwvZGl2PgogICAgICA8ZGl2IHN0eWxlPXt7IHBhZGRpbmdUb3A6IDEwIH19PgogICAgICAgIHsnSW1wb3J0ZWQgZnJvbSDihpAgJ30KICAgICAgICB7LyogPEZvbyAvPiAqL30KICAgICAgICA8ZGl2Pnsn8J+Qt/CfkLcgVE1QIPCfkLfwn5C3IGltcG9ydCBXSVAnfTwvZGl2PgogICAgICAgIDxjb2RlPnsnPEpTWD4g4oaSIFZpdGUg4oaSIEVTTS5qcy5kLnRzIOKGkiBtb2QudHMg4oaSIEpTUiDihpIgaW1wb3J0J308L2NvZGU+CiAgICAgIDwvZGl2PgogICAgICB7ZWxUaGVtZVNhbXBsZX0KICAgIDwvZGl2PgogICk7Cn07Cg==", + "src/-test/entry.tsx": "data:application/typescript+jsx;base64,aW1wb3J0IHsgcGtnIH0gZnJvbSAnLi4vcGtnLnRzJzsKCi8qKgogKiBSZW5kZXIgVUkuCiAqLwpnbG9iYWxUaGlzLmRvY3VtZW50LnRpdGxlID0gcGtnLm5hbWU7CmNvbnNvbGUuaW5mbygn8J+QtyAuL2VudHJ5LnRzeCDihpIgUGtnOvCfkqYnLCBwa2cpOwoKaW1wb3J0IHsgU3RyaWN0TW9kZSB9IGZyb20gJ3JlYWN0JzsKaW1wb3J0IHsgY3JlYXRlUm9vdCB9IGZyb20gJ3JlYWN0LWRvbS9jbGllbnQnOwppbXBvcnQgeyBGb29TYW1wbGUgfSBmcm9tICcuLy1zYW1wbGUvdWkuRm9vLnRzeCc7CgovKioKICog8J+QtyBUZXN0ICIgQHN5cyAiIG1vZHVsZSBpbXBvcnRzIGZyb20gYWNyb3NzIHRoZQogKiAgICBuYW1lc3BhY2UgKG1vbm9yZXBvL3dvcmtzcGFjZSkuCiAqLwovLyBpbXBvcnQgJ0BzeXMvdG1wL3NhbXBsZS1pbXBvcnRzJzsKCi8qKgogKiBTYW1wbGU6IHJlbmRlciByZWFjdCBjb21wb25lbnQuCiAqLwpjb25zdCByb290ID0gY3JlYXRlUm9vdChkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncm9vdCcpISk7CnJvb3QucmVuZGVyKAogIDxTdHJpY3RNb2RlPgogICAgPEZvb1NhbXBsZSBzdHlsZT17eyBib3JkZXI6IGBzb2xpZCAxcHggYmx1ZWAgfX0gLz4KICA8L1N0cmljdE1vZGU+LAopOwo=", "src/-test/index.html": "data:text/plain;base64,PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KICA8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAiIC8+CiAgICA8dGl0bGU+bG9hZGluZy4uLjwvdGl0bGU+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPGRpdiBpZD0icm9vdCI+PC9kaXY+CiAgICA8c2NyaXB0IHR5cGU9Im1vZHVsZSIgc3JjPSIuL2VudHJ5LnRzeCI+PC9zY3JpcHQ+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==", "src/-test/mod.ts": "data:application/typescript;base64,ZXhwb3J0IHsgZGVzY3JpYmUsIGV4cGVjdCwgZXhwZWN0RXJyb3IsIGl0LCBUZXN0aW5nIH0gZnJvbSAnQHN5cy90ZXN0aW5nL3NlcnZlcic7CmV4cG9ydCAqIGZyb20gJy4uL2NvbW1vbi50cyc7Cg==", "src/.test.ts": "data:application/typescript;base64,aW1wb3J0IHsgdHlwZSB0LCBkZXNjcmliZSwgaXQsIGV4cGVjdCwgUGtnLCBwa2cgfSBmcm9tICcuLy10ZXN0LnRzJzsKCmRlc2NyaWJlKGBtb2R1bGU6ICR7UGtnLnRvU3RyaW5nKHBrZyl9YCwgKCkgPT4gewogIGl0KCdleGlzdHMnLCAoKSA9PiB7CiAgICBjb25zb2xlLmluZm8oYPCfkqYgTW9kdWxlYCwgcGtnKTsKICAgIGV4cGVjdCh0eXBlb2YgcGtnLm5hbWUgPT09ICdzdHJpbmcnKS50by5iZS50cnVlOwogIH0pOwp9KTsK", @@ -24,5 +21,5 @@ "src/mod.ts": "data:application/typescript;base64,LyoqCiAqIEBtb2R1bGUKICogVG9vbHMgZm9yLi4u8J+QtwogKi8KZXhwb3J0IHsgcGtnIH0gZnJvbSAnLi9wa2cudHMnOwoKLyoqIE1vZHVsZSB0eXBlcy4gKi8KZXhwb3J0IHR5cGUgKiBhcyB0IGZyb20gJy4vdHlwZXMudHMnOwo=", "src/pkg.ts": "data:application/typescript;base64,aW1wb3J0IHsgUGtnLCB0eXBlIHQgfSBmcm9tICdAc3lzL3N0ZCc7CmltcG9ydCB7IGRlZmF1bHQgYXMgZGVubyB9IGZyb20gJy4uL2Rlbm8uanNvbicgd2l0aCB7IHR5cGU6ICdqc29uJyB9OwoKCi8qKgogKiBQYWNrYWdlIG1ldGEtZGF0YS4KICovCmV4cG9ydCBjb25zdCBwa2c6IHQuUGtnID0gUGtnLmZyb21Kc29uKGRlbm8pOwo=", "src/types.ts": "data:application/typescript;base64,LyoqCiAqIEBtb2R1bGUKICogTW9kdWxlIHR5cGVzLgogKi8KZXhwb3J0IHt9Owo=", - "vite.config.ts": "data:application/typescript;base64,aW1wb3J0IHsgVml0ZSB9IGZyb20gJ0BzeXMvZHJpdmVyLXZpdGUnOwppbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJzsKCmV4cG9ydCBjb25zdCBwYXRocyA9IFZpdGUuQ29uZmlnLnBhdGhzKHsgYXBwOiB7IGVudHJ5OiAnLi9zcmMvLXRlc3QvaW5kZXguaHRtbCcgfSB9KTsKCmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZygoKSA9PiB7CiAgcmV0dXJuIFZpdGUuQ29uZmlnLmFwcCh7CiAgICBwYXRocywKICAgIGNodW5rcyhlKSB7CiAgICAgIGUuY2h1bmsoJ3JlYWN0JywgJ3JlYWN0Jyk7CiAgICAgIGUuY2h1bmsoJ3JlYWN0LmRvbScsICdyZWFjdC1kb20nKTsKICAgICAgZS5jaHVuaygnc3lzJywgWydAc3lzL3N0ZCddKTsKICAgIH0sCiAgfSk7Cn0pOwo=" + "vite.config.ts": "data:application/typescript;base64,aW1wb3J0IHsgVml0ZSB9IGZyb20gJ2pzcjpAc3lzL2RyaXZlci12aXRlJzsKaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAnbnBtOnZpdGUnOwoKZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKCgpID0+IHsKICBjb25zdCBlbnRyeSA9ICcuL3NyYy8tdGVzdC9pbmRleC5odG1sJzsKICBjb25zdCBwYXRocyA9IFZpdGUuQ29uZmlnLnBhdGhzKHsgYXBwOiB7IGVudHJ5IH0gfSk7CiAgcmV0dXJuIFZpdGUuQ29uZmlnLmFwcCh7CiAgICBwYXRocywKICAgIGNodW5rcyhlKSB7CiAgICAgIGUuY2h1bmsoJ3JlYWN0JywgJ3JlYWN0Jyk7CiAgICAgIGUuY2h1bmsoJ3JlYWN0LmRvbScsICdyZWFjdC1kb20nKTsKICAgICAgZS5jaHVuaygnc3lzJywgWydAc3lzL3N0ZCddKTsKICAgICAgZS5jaHVuaygnY3NzJywgWydAc3lzL3VpLWNzcyddKTsKICAgIH0sCiAgfSk7Cn0pOwo=" } diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/.test.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/.test.ts deleted file mode 100644 index 5eecb6486d..0000000000 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SAMPLE, describe, expect, it } from '../-test.ts'; -import { Vite } from '../mod.ts'; - -describe('Vite: Template Generation', () => { - it('inserts template', async () => { - const fs = SAMPLE.fs('Vite.tmpl'); - expect(await fs.ls()).to.eql([]); - - const tmpl = await Vite.Tmpl.create(); - const res = await tmpl.copy(fs.dir); - - const a = (await fs.ls()).toSorted(); - const b = (await res.target.ls()).toSorted(); - - expect(b.length).to.be.greaterThan(10); - expect(a).to.eql(b); - }); -}); diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/m.Tmpl.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/m.Tmpl.ts index fd0bc421c5..a0ca24445a 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/m.Tmpl.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/m.Tmpl.ts @@ -1,10 +1,12 @@ import type { t } from './common.ts'; import { Bundle } from './m.Bundle.ts'; import { create } from './u.create.ts'; -import { update } from './u.update.ts'; +import { prep } from './u.prep.ts'; +import { write } from './u.write.ts'; export const ViteTmpl: t.ViteTmplLib = { Bundle, + prep, create, - update, + write, }; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/t.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/t.ts index 651c293acc..78c2000c5a 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/t.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/t.ts @@ -1,5 +1,8 @@ import type { t } from './common.ts'; +/** Index of strings representing templates variants. */ +export type ViteTmplKind = 'Default' | 'ComponentLib'; + /** * Template Library: * Create (and keep upated) a vanilla "Vite" project. @@ -11,13 +14,25 @@ export type ViteTmplLib = { /** Creates an instance of the template file generator. */ create(args?: t.ViteTmplCreateArgs): Promise<t.Tmpl>; - /** Initialize the local machine environment with latest templates */ - update(args?: t.ViteTmplUpdateArgs): Promise<t.ViteTmplUpdateResponse>; + /** Write and process the templates to the local file-system. */ + write(args?: t.ViteTmplWriteArgs): Promise<t.ViteTmplWriteResponse>; + + /** Prepare the template with latest state and dependency versions. */ + prep(options?: { silent?: boolean }): Promise<t.ViteTmplPrepResponse>; }; /** Arguments passed to the `ViteTmpl.create` method. */ export type ViteTmplCreateArgs = { version?: t.StringSemver; + tmpl?: t.ViteTmplKind; +}; + +/** + * The context object passed to the template file-processor. + */ +export type ViteTmplCtx = { + version: t.StringSemver; + tmpl: t.ViteTmplKind; }; /** @@ -32,14 +47,16 @@ export type ViteBundleLib = { }; /** Arguments passed to the `Vite.Tmpl.update` method. */ -export type ViteTmplUpdateArgs = { +export type ViteTmplWriteArgs = { force?: boolean; in?: t.StringDir; version?: t.StringSemver; + tmpl?: t.ViteTmplKind; silent?: boolean; }; -/** - * The response returned from an environment update. - */ -export type ViteTmplUpdateResponse = { readonly ops: t.TmplFileOperation[] }; +/** The response returned from an environment update. */ +export type ViteTmplWriteResponse = { readonly ops: t.TmplFileOperation[] }; + +/** The response returned from the `Vite.Tmpl.prep` method */ +export type ViteTmplPrepResponse = { readonly deps: t.EsmImportMap }; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.create.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.create.ts index bdfe6a69e6..32c9097268 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.create.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.create.ts @@ -1,4 +1,4 @@ -import { type t, Fs, PATHS, Tmpl } from './common.ts'; +import { type t, Fs, PATHS, Tmpl, pkg } from './common.ts'; import { Bundle } from './m.Bundle.ts'; import { createFileProcessor } from './u.process.file.ts'; @@ -6,12 +6,13 @@ import { createFileProcessor } from './u.process.file.ts'; * Create a new instance of the bundled file template. */ export const create: t.ViteTmplLib['create'] = async (args = {}) => { + const ctx = wrangle.ctx(args); const templatesDir = Fs.resolve(PATHS.tmpl.tmp); /** * Ensure the templates are hydrated and ready to use. */ - const beforeCopy: t.TmplCopyHandler = async () => { + const beforeWrite: t.TmplWriteHandler = async () => { await Fs.remove(templatesDir); await Bundle.writeToFile(templatesDir); }; @@ -19,11 +20,21 @@ export const create: t.ViteTmplLib['create'] = async (args = {}) => { /** * (š·) Perform additional setup here (as needed). */ - const afterCopy: t.TmplCopyHandler = async (e) => {}; + const afterWrite: t.TmplWriteHandler = async (e) => {}; /** * Template-engine instance. */ const processFile = createFileProcessor(args); - return Tmpl.create(templatesDir, { processFile, beforeCopy, afterCopy }); + return Tmpl.create(templatesDir, { processFile, beforeWrite, afterWrite, ctx }); }; + +/** + * Helpers + */ +const wrangle = { + ctx(args: t.ViteTmplCreateArgs): t.ViteTmplCtx { + const { version = pkg.version, tmpl = 'Default' } = args; + return { version, tmpl }; + }, +} as const; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.prep.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.prep.ts new file mode 100644 index 0000000000..086e256ac7 --- /dev/null +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.prep.ts @@ -0,0 +1,31 @@ +import { type t, c, Cli, Esm, Fs, getWorkspaceModules, Semver } from './common.ts'; + +/** + * Prepare the template with latest state, including making updates to deps/versions. + */ +export const prep: t.ViteTmplLib['prep'] = async (options = {}) => { + const { modules } = await getWorkspaceModules(); + const path = './src/-tmpl/imports.json'; + const current = (await Fs.readJson<t.DenoImportMapJson>(path)).data; + const imports = modules.latest(current?.imports ?? {}); + await Fs.writeJson(path, { ...current, imports }); + + if (!options.silent) { + const table = Cli.table([]); + Object.entries(imports).forEach(([key, value]) => { + const m = Esm.parse(value); + const pkg = c.gray(` ${key}`); + const registry = c.gray(m.registry.toUpperCase()); + const version = Semver.Fmt.colorize(m.version); + table.push([pkg, version, registry]); + }); + + console.info(); + console.info(c.italic(c.gray('imports.json'))); + console.info(c.brightGreen(`Dependencies:`)); + console.info(table.toString()); + console.info(); + } + + return { deps: imports }; +}; diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.process.file.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.process.file.ts index 56c9c7af69..9ca2bfb19d 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.process.file.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.process.file.ts @@ -1,21 +1,13 @@ -import { Main } from '@sys/main/cmd'; -import { type t, c, DenoDeps, DenoFile, Esm, Fs, Path, pkg, Pkg } from './common.ts'; +import { type t, pkg } from './common.ts'; /** * File processing rules for the template. */ export function createFileProcessor(args: t.ViteTmplCreateArgs): t.TmplProcessFile { - console.log(`ā”ļøš¦š·š³š¦ šš§Øš¼āØš§« ššš§ ā ļø š„šļøš”ā⢠āāāāā`); - console.log('args', args); - - const getWorkspace = async () => { - const ws = await DenoFile.workspace(); - const deps = (await DenoDeps.from(Path.join(ws.dir, 'deps.yaml'))).data; - const modules = Esm.modules([...(deps?.modules.items ?? []), ...ws.modules.items]); - return { ws, modules }; - }; - return async (e) => { + const ctx = e.ctx as t.ViteTmplCtx; + if (!ctx) throw new Error(`Expected a {ctx} to be passed to the template file processor`); + if (e.target.exists && is.userspace(e.target.relative)) { /** * 𫵠DO NOT adjust user generated @@ -24,39 +16,18 @@ export function createFileProcessor(args: t.ViteTmplCreateArgs): t.TmplProcessFi return e.exclude('user-space'); } + if (e.contentType !== 'text') return; + if (e.target.relative === 'deno.json') { /** * Update versions in `deno.json`: */ const version = args.version ?? pkg.version; - const importUri = `jsr:${pkg.name}@${version}`; - const text = e.text.tmpl - .replace(/<ENTRY>/g, `${importUri}/main`) - .replace(/<ENTRY_SYS>/, `jsr:${Pkg.toString(Main.pkg)}`) - .replace(/<SELF_IMPORT_URI>/, importUri) - .replace(/<SELF_IMPORT_NAME>/, pkg.name); - + const entryUri = `jsr:${pkg.name}@${version}`; + const text = e.text.tmpl.replace(/<ENTRY>/g, `${entryUri}/main`); return e.modify(text); } - if (e.target.relative === 'package.json') { - const { modules } = await getWorkspace(); - const pkg = (await Fs.readJson<t.PkgJsonNode>(e.tmpl.absolute)).data; - const next = { - ...pkg, - dependencies: modules.latest(pkg?.dependencies ?? {}), - devDependencies: modules.latest(pkg?.devDependencies ?? {}), - }; - - console.info(c.gray(`Resolved versions:`)); - console.info(c.brightCyan(c.bold(`./package.json:`))); - console.info(next); - console.info(); - - const json = `${JSON.stringify(next, null, ' ')}\n`; - return e.modify(json); - } - if (e.target.file.name === '.gitignore-') { /** * Rename to ".gitignore" diff --git a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.update.ts b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.write.ts similarity index 71% rename from code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.update.ts rename to code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.write.ts index 2dd2ee990d..13068677a2 100644 --- a/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.update.ts +++ b/code/sys.driver/driver-vite/src/m.Vite.Tmpl/u.write.ts @@ -2,17 +2,17 @@ import { type t, c, Fs, Tmpl } from './common.ts'; import { create } from './u.create.ts'; /** - * Initialize the local machine environment with latest templates + * Write and process the templates to the local file-system. */ -export const update: t.ViteTmplLib['update'] = async (args = {}) => { +export const write: t.ViteTmplLib['write'] = async (args = {}) => { const { version, force = false, silent = false } = args; /** * Update template files. */ - const tmpl = await create({ version }); + const tmpl = await create({ version, tmpl: args.tmpl ?? 'Default' }); const dir = args.in ?? '.'; - const { ops } = await tmpl.copy(dir, { force }); + const { ops } = await tmpl.write(dir, { force }); /** * 𫵠Clean up helpers here (flesh out as needed: š·). diff --git a/code/sys.driver/driver-vite/src/m.Vite/-build.test.ts b/code/sys.driver/driver-vite/src/m.Vite/-build.test.ts index 40fac59a02..f292d51ec6 100644 --- a/code/sys.driver/driver-vite/src/m.Vite/-build.test.ts +++ b/code/sys.driver/driver-vite/src/m.Vite/-build.test.ts @@ -31,9 +31,10 @@ describe('Vite.build', () => { const testBuild = async (sample: t.StringDir) => { const fs = SAMPLE.fs('Vite.build'); + await Fs.copy(sample, fs.dir); + const cwd = fs.dir; - await Fs.copy(sample, cwd); - const m = await Vite.Config.fromFile(Fs.join(cwd, 'vite.config.ts')); + const fromFile = await Vite.Config.fromFile(Fs.join(cwd, 'vite.config.ts')); const res = await Vite.build({ cwd, pkg }); if (!res.ok) console.warn(res.toString()); @@ -42,7 +43,7 @@ describe('Vite.build', () => { expect(res.cmd.input).to.include('deno run'); expect(res.cmd.input).to.include('--node-modules-dir npm:vite'); expect(res.elapsed).to.be.greaterThan(0); - expect(res.paths).to.eql(m.module.paths); + expect(res.paths).to.eql(fromFile.paths); // Ensure the {pkg:name:version} data is included in the composite <digest> hash. const keys = Object.keys(res.dist.hash.parts); @@ -69,7 +70,6 @@ describe('Vite.build', () => { it('sample-1: simple', async () => { const { res, files, outDir } = await testBuild(SAMPLE.Dirs.sample1); - printHtml(files.html, 'sample-1', outDir); expect(files.html).to.include(`<title>Sample-1</title>`); expect(files.entry).to.include(`Hello World š`); diff --git a/code/sys.driver/driver-vite/src/m.Vite/t.ts b/code/sys.driver/driver-vite/src/m.Vite/t.ts index c47f4bcfcf..4b0c95db7f 100644 --- a/code/sys.driver/driver-vite/src/m.Vite/t.ts +++ b/code/sys.driver/driver-vite/src/m.Vite/t.ts @@ -41,8 +41,9 @@ export type ViteLib = { */ export type ViteBuildArgs = { cwd?: t.StringAbsoluteDir; - silent?: boolean; pkg?: t.Pkg; // Consumer module. + silent?: boolean; + spinner?: boolean; }; /** diff --git a/code/sys.driver/driver-vite/src/m.Vite/u.build.ts b/code/sys.driver/driver-vite/src/m.Vite/u.build.ts index 81c03955c0..ee79df04ad 100644 --- a/code/sys.driver/driver-vite/src/m.Vite/u.build.ts +++ b/code/sys.driver/driver-vite/src/m.Vite/u.build.ts @@ -1,4 +1,4 @@ -import { type t, Fs, Pkg, Process, Time } from './common.ts'; +import { type t, c, Cli, Fs, Pkg, Process, Time } from './common.ts'; import { Log, Wrangle } from './u.ts'; type B = t.ViteLib['build']; @@ -10,11 +10,28 @@ export const build: B = async (input) => { const timer = Time.timer(); const paths = await Wrangle.pathsFromConfigfile(input.cwd); - const { pkg, silent = true } = input; + const { pkg, silent = false } = input; const { cmd, args } = await Wrangle.command(paths, 'build'); const dir = Fs.join(paths.cwd, paths.app.outDir); + const cwd = paths.cwd; - const output = await Process.invoke({ args, silent }); + if (!silent) { + const table = Cli.table([]); + const push = (label: string, ...value: string[]) => table.push([c.gray(label), ...value]); + push('Directory:', c.gray(`${cwd.replace(/\/$/, '')}/`)); + push(' - entry:', paths.app.entry); + push(' - outDir:', paths.app.outDir); + push(' - base:', paths.app.base); + + console.info(c.bold(c.brightGreen('Paths'))); + console.info(table.toString().trim()); + console.info(); + } + + const spinner = Cli.Spinner.create('building', { silent, start: false }); + if ((input.spinner ?? true) && !silent) spinner.start(); + + const output = await Process.invoke({ cwd, args, silent: true }); const ok = output.success; if (pkg) { @@ -47,6 +64,7 @@ export const build: B = async (input) => { }, }; + spinner.stop(); return res; }; diff --git a/code/sys.driver/driver-vite/src/m.Vite/u.dev.ts b/code/sys.driver/driver-vite/src/m.Vite/u.dev.ts index 809fb98cda..738e05e0e2 100644 --- a/code/sys.driver/driver-vite/src/m.Vite/u.dev.ts +++ b/code/sys.driver/driver-vite/src/m.Vite/u.dev.ts @@ -4,12 +4,11 @@ import { Log, Wrangle } from './u.ts'; type D = t.ViteLib['dev']; -/** - * Matches (example): - * VITE v6.0.11 ready in 839 ms - */ export const REGEX = { - VITE_STARTED: /VITE v(?:\d+\.\d+\.\d+)\s+ready in\s+(\d+)\s+ms/, + /** + * Matches (example): "VITE v6.0.11 ready in 839 ms" + */ + STARTED: /VITE v(?:\d+\.\d+\.\d+)\s+ready in\s+(\d+)\s+ms/, } as const; /** @@ -32,7 +31,7 @@ export const dev: D = async (input) => { const readySignal: t.ProcReadySignalFilter = (e) => { const lines = stripAnsi(e.toString()).split('\n'); - return lines.some((line) => !!REGEX.VITE_STARTED.exec(line)); + return lines.some((line) => !!REGEX.STARTED.exec(line)); }; const proc = Process.spawn({ cwd, args, silent, readySignal, dispose$: input.dispose$ }); diff --git a/code/sys.driver/driver-vite/src/m.Vite/u.wrangle.ts b/code/sys.driver/driver-vite/src/m.Vite/u.wrangle.ts index 18be139141..cab1e64dfa 100644 --- a/code/sys.driver/driver-vite/src/m.Vite/u.wrangle.ts +++ b/code/sys.driver/driver-vite/src/m.Vite/u.wrangle.ts @@ -17,7 +17,7 @@ export const Wrangle = { const path = Path.join(rootDir, filename); const res = await ViteConfig.fromFile(path); - let paths = res.module.paths; + let paths = res.paths; if (!paths) { const err = `Failed to load paths from [${filename}], ensure it exports "paths". Source: ${path}`; diff --git a/code/sys.driver/driver-vite/src/pkg.ts b/code/sys.driver/driver-vite/src/pkg.ts index 79cb3f9c80..3ed982332f 100644 --- a/code/sys.driver/driver-vite/src/pkg.ts +++ b/code/sys.driver/driver-vite/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-vite', version: '0.0.137' }; diff --git a/code/sys.driver/driver-vite/vite.config.ts b/code/sys.driver/driver-vite/vite.config.ts deleted file mode 100644 index 63c4880869..0000000000 --- a/code/sys.driver/driver-vite/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @module - * š· (proxy export sample) - */ -export * from './src/-test/vite.sample-config/custom/vite.config.ts'; -export { default } from './src/-test/vite.sample-config/custom/vite.config.ts'; - -// export * from './src/-test/vite.sample-config/config.simple.ts'; -// export { default } from './src/-test/vite.sample-config/config.simple.ts'; diff --git a/code/sys.driver/driver-vitepress/-scripts/-prep.ts b/code/sys.driver/driver-vitepress/-scripts/-prep.ts index 9182a8614c..bb242d6f1c 100644 --- a/code/sys.driver/driver-vitepress/-scripts/-prep.ts +++ b/code/sys.driver/driver-vitepress/-scripts/-prep.ts @@ -1,25 +1,22 @@ -import { Vitepress } from '@sys/driver-vitepress'; -import { type t, c, DenoDeps, DenoFile, Fs, PATHS, pkg } from './common.ts'; +import { Vitepress } from '../src/mod.ts'; +import { c, Fs, PATHS, pkg, Semver } from './common.ts'; const resolve = (...parts: string[]) => Fs.join(import.meta.dirname ?? '', '..', ...parts); await Fs.remove(resolve('.tmp')); /** - * Save monorepo deps. + * Update to latest dependency versions. */ -const ws = await DenoFile.workspace(); -const deps: t.Dep[] = ws.modules.items.map((esm) => DenoDeps.toDep(esm)); - -const dir = resolve('src/-tmpl/.sys'); -await Fs.copy(Fs.join(ws.dir, 'deps.yaml'), Fs.join(dir, 'deps.yaml'), { force: true }); -await Fs.write(Fs.join(dir, 'deps.sys.yaml'), DenoDeps.toYaml(deps).text); +await Vitepress.Tmpl.prep(); /** - * Bundle files (for code-registry). + * Bundle files inline, base64-string FileMap (NB: so code can be referenfed within the registry). */ const Bundle = Vitepress.Tmpl.Bundle; await Bundle.toFilemap(); await Bundle.toFilesystem(resolve(PATHS.tmpl.tmp)); // NB: test output. -console.info(c.brightCyan('ā Prep Complete:'), `${pkg.name}@${c.brightCyan(pkg.version)}`); +const fmtVersion = Semver.Fmt.colorize(pkg.version); +const fmtModule = `${pkg.name}${c.dim('@')}${fmtVersion}`; +console.info(c.brightCyan('ā Prep Complete:'), fmtModule); console.info(); diff --git a/code/sys.driver/driver-vitepress/deno.json b/code/sys.driver/driver-vitepress/deno.json index 8d37a830b5..42f146bfee 100644 --- a/code/sys.driver/driver-vitepress/deno.json +++ b/code/sys.driver/driver-vitepress/deno.json @@ -1,21 +1,21 @@ { "name": "@sys/driver-vitepress", - "version": "0.0.284-alpha.2", + "version": "0.0.302", "license": "MIT", "tasks": { "lint": "deno lint", "dry": "deno publish --allow-dirty --dry-run", - "reset": "deno run -RWE ./-scripts/-reset.ts", - "test": "deno test -RWNE --allow-run --allow-ffi", + "test": "deno test -RWNE --allow-run --allow-ffi --allow-sys", "init": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=init --dir=./.tmp/sample", "dev": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=dev --dir=./.tmp/sample", "build": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=build --dir=./.tmp/sample", "serve": "deno run -RNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=serve --dir=./.tmp/sample/dist", - "clean": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=clean --dir=./.tmp/sample", "upgrade": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=upgrade --dir=./.tmp/sample", "backup": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=backup --dir=./.tmp/sample", "help": "deno run -RE --allow-ffi ./-scripts/-main.ts --cmd=help --dir=./.tmp/sample", "prep": "deno run -RWE --allow-ffi ./-scripts/-prep.ts", + "clean": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=clean --dir=./.tmp/sample", + "reset": "deno run -RWE ./-scripts/-reset.ts", "r": "deno task reset && deno task prep && deno task init", "rdev": "deno task r && deno task dev", "tmp": "deno run -A ./-scripts/-tmp.ts" @@ -25,7 +25,6 @@ "./t": "./src/types.ts", "./types": "./src/types.ts", "./main": "./src/-entry/-main.ts", - "./init": "./src/-entry/-init.ts", - "./ui": "./src/ui/mod.ts" + "./init": "./src/-entry/-init.ts" } } diff --git a/code/sys.driver/driver-vitepress/src/-entry/m.Entry.ts b/code/sys.driver/driver-vitepress/src/-entry/m.Entry.ts index a89b923102..072cf95b3d 100644 --- a/code/sys.driver/driver-vitepress/src/-entry/m.Entry.ts +++ b/code/sys.driver/driver-vitepress/src/-entry/m.Entry.ts @@ -8,8 +8,8 @@ import { PATHS, pkg, ViteEntry, - VitepressLog, ViteLog, + VitepressLog, } from './common.ts'; type F = t.VitepressEntryLib['main']; @@ -52,10 +52,10 @@ export const VitepressEntry: t.VitepressEntryLib = { if (args.cmd === 'build') { ViteLog.API.log({ cmd: 'build' }); console.info(); - const { dir = PATHS.inDir } = args; const res = await Vitepress.build({ inDir: dir, pkg, silent: false }); - console.info(res.toString({ pad: true })); + console.info(res.toString()); + console.info(); return; } diff --git a/code/sys.driver/driver-vitepress/src/-entry/u.init.ts b/code/sys.driver/driver-vitepress/src/-entry/u.init.ts index b482e4f8b7..17b8676832 100644 --- a/code/sys.driver/driver-vitepress/src/-entry/u.init.ts +++ b/code/sys.driver/driver-vitepress/src/-entry/u.init.ts @@ -1,5 +1,5 @@ import { Vitepress } from '../m.Vitepress/mod.ts'; -import { type t, c, PATHS, pkg, ViteLog } from './common.ts'; +import { type t, c, PATHS, pkg, Semver, ViteLog } from './common.ts'; /** * Run the initialization templates. @@ -13,13 +13,17 @@ export async function init(args: t.VitepressEntryArgsInit) { console.info(`${pkg.name} ${c.gray(pkg.version)}`); } - await Vitepress.Tmpl.update({ inDir: dir }); + await Vitepress.Tmpl.write({ inDir: dir }); if (!silent) { console.info(); ViteLog.API.log(); + + const fmtVersion = Semver.Fmt.colorize(pkg.version); + const fmtModule = `${pkg.name}${c.dim('@')}${fmtVersion}`; + console.info(); - console.info(c.brightCyan('ā Init Complete:'), `${pkg.name}@${c.brightCyan(pkg.version)}`); + console.info(c.brightCyan('ā Init Complete:'), `${fmtModule}`); console.info(); } } diff --git a/code/sys.driver/driver-vitepress/src/-test/tmpl.tests/-Global.test.ts b/code/sys.driver/driver-vitepress/src/-test/tmpl.tests/-Global.test.ts new file mode 100644 index 0000000000..22af1c9fd9 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-test/tmpl.tests/-Global.test.ts @@ -0,0 +1,44 @@ +import { type t, describe, expect, it } from '../../-test.ts'; +import { Global } from '../../-tmpl/.sys/mod.ts'; + +import { type t as tt } from '../../-tmpl/.sys/common.ts'; + +describe('Global (State)', () => { + type G = tt.GlobalState; + + it('default (singleton)', () => { + const a = Global.state(); + const b = Global.state(); + expect(a).to.equal(b); // NB: same instance + }); + + it('custom: instance-id', () => { + const id = 'foo'; + const a = Global.state(id); + const b = Global.state(id); + const c = Global.state(); + const d = Global.state('something else'); + expect(a).to.equal(b); // NB: same instance + expect(a).to.not.equal(c); + expect(a).to.not.equal(d); + }); + + it('change', () => { + const a = Global.state(); + const b = Global.state(); + expect(a.current.tmp).to.eql(0); + a.change((d) => d.tmp++); + expect(b.current.tmp).to.eql(1); + }); + + it('shared events', () => { + const a = Global.state(); + const b = Global.state(); + const bEvents = a.events(); + const bFired: tt.GlobalStateEvent[] = []; + bEvents.changed$.pipe().subscribe((e) => bFired.push(e)); + + a.change((d) => d.tmp++); + expect(bFired[0].after).to.eql(a.current); + }); +}); diff --git a/code/sys.driver/driver-vitepress/src/-test/tmpl.tests/-Props.test.ts b/code/sys.driver/driver-vitepress/src/-test/tmpl.tests/-Props.test.ts new file mode 100644 index 0000000000..165b8f170f --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-test/tmpl.tests/-Props.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from '../../-test.ts'; +import { Props } from '../../-tmpl/.sys/mod.ts'; + +describe('Props', () => { + it('encode ā decode', () => { + const props = { foo: 123, bar: { msg: 'š hello' } }; + const a = Props.encode(props); + const b = Props.decode(a); + expect(a).to.include('123,34,102,111,111,34,58,49,50,51,44,34,98,97,114,34,58'); + expect(b).to.eql(props); + }); + + describe('decode', () => { + it('invalid input', () => { + const NON = [123, true, null, undefined, BigInt(0), Symbol('foo'), {}, []]; + NON.forEach((value: any) => expect(Props.decode(value)).to.eql({})); + }); + + it('empty string', () => { + const test = (input: string) => { + expect(Props.decode(input)).to.eql({}); + }; + test(''); + test(' '); + }); + + it('corrupt binary', () => { + const res = Props.decode('123,34,102,111,111,34,58'); + expect(res).to.eql({}); + }); + + it('not JSON', () => { + const res = Props.decode('foobar'); + expect(res).to.eql({}); + }); + }); +}); diff --git a/code/sys.driver/driver-vitepress/src/-test/u.assert.ts b/code/sys.driver/driver-vitepress/src/-test/u.assert.ts index b71dfc2858..23c87eb225 100644 --- a/code/sys.driver/driver-vitepress/src/-test/u.assert.ts +++ b/code/sys.driver/driver-vitepress/src/-test/u.assert.ts @@ -21,7 +21,6 @@ export const assertEnvExists = async (dir: t.StringDir, expected = true) => { '.vitepress/config.ts', '.vitepress/config.markdown.ts', '.vitepress/theme/index.ts', - '.sys/-main.ts', '.sys/components/index.ts', '.sys/components/Video.vue', '.sys/components/Video.tsx', diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/-main.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/-main.ts deleted file mode 100644 index 31dd5981c3..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/-main.ts +++ /dev/null @@ -1 +0,0 @@ -import 'jsr:@sys/driver-vitepress/main'; diff --git a/code/sys.driver/driver-vitepress/src/ui/common.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common.ts similarity index 100% rename from code/sys.driver/driver-vitepress/src/ui/common.ts rename to code/sys.driver/driver-vitepress/src/-tmpl/.sys/common.ts diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/libs.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/libs.ts new file mode 100644 index 0000000000..27b12bc00e --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/libs.ts @@ -0,0 +1 @@ +export { Immutable, slug } from '@sys/std'; diff --git a/deploy/slc.db.team/src/common/mod.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/mod.ts similarity index 64% rename from deploy/slc.db.team/src/common/mod.ts rename to code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/mod.ts index 1c5667a854..8cf7cac86f 100644 --- a/deploy/slc.db.team/src/common/mod.ts +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/mod.ts @@ -1,3 +1,2 @@ -export { pkg } from '../pkg.ts'; export * from './libs.ts'; export type * as t from './t.ts'; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/t.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/t.ts new file mode 100644 index 0000000000..3f57d44e25 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/common/t.ts @@ -0,0 +1,2 @@ +export type * from '@sys/types'; +export type * from '../types.ts'; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.Wrapper.Sample.tsx b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.Wrapper.Sample.tsx deleted file mode 100644 index 141de488de..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.Wrapper.Sample.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-types="@types/react" -import React from 'react'; - -export type MyComponentProps = { - count?: number; -}; - -export const MyComponent: React.FC<MyComponentProps> = (props) => { - console.log('MyComponent.props:', props); - return ( - <div style={{ marginTop: 5, padding: 10, backgroundColor: 'rgba(255, 0, 0, 0.1)' /* RED */ }}> - <div>Hello from React š</div> - </div> - ); -}; -// diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.Wrapper.vue b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.Wrapper.vue deleted file mode 100644 index 72c089742d..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.Wrapper.vue +++ /dev/null @@ -1,14 +0,0 @@ - -<template> - <div ref="root"></div> -</template> - -<script setup lang="ts"> -import { setup, ref } from './React.setup'; -import { MyComponent } from './React.Wrapper.Sample'; - -const root = ref(); -setup(root, MyComponent, { count: 1234 }); -</script> - -<style scoped></style> diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.setup.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.setup.ts deleted file mode 100644 index 13f934e060..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/React.setup.ts +++ /dev/null @@ -1,31 +0,0 @@ - -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { onBeforeUnmount, onMounted, ref as vueRef, type Ref } from 'vue'; - -type O = Record<string, unknown>; - -export const ref = () => vueRef<HTMLElement | undefined>(); - -/** - * Setup a react-in-vue wrapper. - */ -export function setup<P extends O>( - refRoot: Ref<HTMLElement | undefined>, - Component: React.FC<P>, - props?: P, -) { - let root: ReactDOM.Root | undefined; - - onMounted(() => { - if (refRoot.value) { - const el = React.createElement(Component, props); - root = ReactDOM.createRoot(refRoot.value); - root.render(el); - } - }); - - onBeforeUnmount(() => { - root?.unmount(); - }); -} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/Video.tsx b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/Video.tsx deleted file mode 100644 index c4fc667f69..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/Video.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-types="@types/react" -import React from 'react'; - -import '@sys/tmp/sample-imports'; -import { Foo, VideoPlayer } from '@sys/tmp/ui'; - -export const DEFAULTS = { - src: 'vimeo/499921561', // Tubes. -} as const; - -export type VideoProps = { - title?: string; - src?: string; -}; - -/** - * Component - */ -export const Video: React.FC<VideoProps> = (props: VideoProps) => { - const src = props.src || DEFAULTS.src; - return ( - <div> - <Foo /> - <VideoPlayer /> - </div> - ); -}; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/Video.vue b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/Video.vue deleted file mode 100644 index 8abf06d334..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/Video.vue +++ /dev/null @@ -1,20 +0,0 @@ - -<template> - <div ref="root" class="root"></div> -</template> - -<script setup lang="ts"> -import { setup, ref } from './React.setup'; -import { Video } from './Video'; - -type VideoProps = { title?: string; src?: string }; -const root = ref(); -const props = defineProps<VideoProps>(); -setup(root, Video, props); -</script> - -<style scoped> -.root { - padding-bottom: 10px; -} -</style> diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/index.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/index.ts deleted file mode 100644 index 2dbf0434e9..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { EnhanceAppContext } from 'vitepress'; - -import ReactWrapper from './React.Wrapper.vue'; -import Video from './Video.vue'; - -export function registerComponents(ctx: EnhanceAppContext) { - ctx.app.component('Video', Video); - ctx.app.component('ReactWrapper', ReactWrapper); -} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/deps.sys.yaml b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/deps.sys.yaml deleted file mode 100644 index 290c766cd9..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/deps.sys.yaml +++ /dev/null @@ -1,36 +0,0 @@ -deno.json: - - import: jsr:@sys/types@0.0.82 - - import: jsr:@sys/std@0.0.131 - - import: jsr:@sys/color@0.0.35 - - import: jsr:@sys/testing@0.0.75 - - import: jsr:@sys/fs@0.0.79 - - import: jsr:@sys/cli@0.0.64 - - import: jsr:@sys/process@0.0.65 - - import: jsr:@sys/crypto@0.0.64 - - import: jsr:@sys/http@0.0.46 - - import: jsr:@sys/text@0.0.73 - - import: jsr:@sys/tmpl@0.0.79 - - import: jsr:@sys/cmd@0.0.80 - - import: jsr:@sys/jsr@0.0.45 - - import: jsr:@sys/ui-css@0.0.70 - - import: jsr:@sys/ui-dom@0.0.77 - - import: jsr:@sys/ui-react@0.0.84 - - import: jsr:@sys/ui-react-devharness@0.0.82 - - import: jsr:@sys/ui-react-components@0.0.38 - - import: jsr:@sys/driver-automerge@0.0.84 - - import: jsr:@sys/driver-deno@0.0.86 - - import: jsr:@sys/driver-immer@0.0.86 - - import: jsr:@sys/driver-obsidian@0.0.73 - - import: jsr:@sys/driver-ollama@0.0.40 - - import: jsr:@sys/driver-orbiter@0.0.63 - - import: jsr:@sys/driver-quilibrium@0.0.76 - - import: jsr:@sys/driver-vite@0.0.121 - - import: jsr:@sys/driver-vitepress@0.0.284-alpha.2 - - import: jsr:@sys/sys@0.0.55 - - import: jsr:@sys/main@0.0.57 - - import: jsr:@tdb/api@0.0.72 - - import: jsr:@tdb/slc@0.0.68 - - import: jsr:@tdb/tmp@0.0.72 - - import: jsr:@sys/name@0.0.0 - - import: jsr:@sys/tmp@0.0.95 -package.json: [] diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/deps.yaml b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/deps.yaml deleted file mode 100644 index 9bd188f371..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/deps.yaml +++ /dev/null @@ -1,117 +0,0 @@ -# -# System Dependencies ("imports") -# -# ./š¦ -# | deno.json -# |(write) ā deno.imports.json -# |(write) ā package.json -# -# This is the "single-source-of-truth" with regards to dependencies and versioning. -# Import maps (in the `deno.json` and `package.json` files) are auto-generated -# from this config definition. -# -# Also, as a programmatic API, other downstream dependencies -# (such as template generators, see `@sys/tmpl`) use this definition -# file to calculate the "latest" versions to inject into, say, -# a `package.json` file for a scaffolded project. -# - -groups: - std/deno: - # Deno standard libs ("std"). - - import: jsr:@std/async@1.0.10 - - import: jsr:@std/datetime@0.225.3 - - import: jsr:@std/dotenv@0.225.3 - - import: jsr:@std/encoding@1.0.7 - - import: jsr:@std/fs@1.0.13 - - import: jsr:@std/path@1.0.8 - - import: jsr:@std/semver@1.0.4 - - import: jsr:@std/testing@1.0.9 - - import: jsr:@std/uuid@1.0.4 - - automerge: - # https://automerge.org - - import: npm:@automerge/automerge@2.2.8 - - import: npm:@automerge/automerge-repo@1.2.1 - - import: npm:@automerge/automerge-repo-network-broadcastchannel@1.2.1 - - import: npm:@automerge/automerge-repo-storage-indexeddb@1.2.1 - - import: npm:@automerge/automerge-repo-storage-nodefs@1.2.1 - - import: npm:@onsetsoftware/automerge-patcher@0.14.0 - - crypto: - - import: npm:@noble/hashes@1.7.1 - wildcard: true - - build/tools: - - import: npm:@vitejs/plugin-react-swc@3.8.0 - - import: npm:rollup@4.34.8 - - import: npm:vite@6.1.1 - - import: npm:vite-plugin-wasm@3.4.1 - - ui/react: - - import: npm:@types/react@18.3.18 - - import: npm:@types/react-dom@18.3.5 - - import: npm:react@18.3.1 - - import: npm:react-dom@18.3.1 - -deno.json: - - group: std/deno - - group: crypto - - group: automerge - - # CLI tools - - import: jsr:@cliffy/keypress@1.0.0-rc.7 - - import: jsr:@cliffy/prompt@1.0.0-rc.7 - - import: jsr:@cliffy/table@1.0.0-rc.7 - - # Sundry: NPM - - import: npm:@types/diff@7.0.1 - - import: npm:chai@5 - - import: npm:approx-string-match@2 - - import: npm:date-fns@4 - - import: npm:subhosting@0.1.0-alpha.1 - - import: npm:diff@7 - - import: npm:fast-json-patch@3.1.1 - - import: npm:fake-indexeddb@6.0.0 - - import: npm:happy-dom@17.1.3 - - import: npm:hash-it@6.0.0 - - import: npm:ignore@7 - - import: npm:immer@10 - - import: npm:ora@8.2.0 - - import: npm:ollama@0.5.13 - - import: npm:pretty-bytes@6.1.1 - - import: npm:ramda@0.30.1 - - import: npm:rambda@9.4.2 - - import: npm:rxjs@7.8.2 - - import: npm:strip-ansi@7 - - import: npm:subhosting@0.1.0-alpha.1 - - import: npm:tinycolor2@1.6.0 - - import: npm:ts-essentials@10.0.4 - - import: npm:valibot@1.0.0-rc.1 - - import: npm:yaml@2.7.0 - - # Browser - - import: npm:csstype@3 - - import: npm:ua-parser-js@2.0.2 - - # UI - - import: npm:react-error-boundary@5 - - import: npm:react-inspector@6 - - import: npm:react-spinners@0.15.0 - -package.json: - - group: std/deno - - group: crypto - - group: build/tools - dev: true - - - import: npm:hono@4.7.2 - - # UI - - group: ui/react - - import: npm:react-icons@5.5.0 - - import: npm:@vidstack/react@1.12.12 - - # UI:Frameworks - - import: npm:vitepress@1.6.3 - - import: npm:vue@3.5.13 diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/mod.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/mod.ts new file mode 100644 index 0000000000..f137b8fe9c --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/mod.ts @@ -0,0 +1,2 @@ +export { Props } from './u/u.Props.ts'; +export { Global } from './u/u.Global.ts'; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/types.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/types.ts new file mode 100644 index 0000000000..077dd7213a --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/types.ts @@ -0,0 +1,5 @@ +/** + * @module + * System types + */ +export type * from './u/t.ts'; diff --git a/code/sys.driver/driver-vitepress/src/ui/components/common.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/common.ts similarity index 100% rename from code/sys.driver/driver-vitepress/src/ui/components/common.ts rename to code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/common.ts diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/t.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/t.ts new file mode 100644 index 0000000000..4b1abcee50 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/t.ts @@ -0,0 +1,14 @@ +import type { t } from './common.ts'; + +export type GlobalState = { + tmp: number; +}; + +/** + * Immutable wrapper. + */ +type P = t.PatchOperation; +export type GlobalStateImmutable = t.ImmutableRef<t.GlobalState, P, GlobalStateEvents>; + +export type GlobalStateEvents = t.ImmutableEvents<GlobalState, P>; +export type GlobalStateEvent = t.InferImmutableEvent<t.GlobalStateEvents>; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/u.Global.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/u.Global.ts new file mode 100644 index 0000000000..3d91316744 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/u.Global.ts @@ -0,0 +1,27 @@ +import { type t, Immutable, slug } from './common.ts'; + +const defaultId = `default:${slug()}`; +const refs = new Map<string, t.GlobalStateImmutable>(); + +/** + * Global state interface. + */ +export const Global = { + /** + * Retrieve an instance of the global state object. + * + * @param instance - Optional unique identifier for the global state instance. + * If not provided, the default instance is returned. + * No param is the equivalent of retrieving the "singlton" instance. + * + * @returns The global state instance corresponding to the provided identifier. + */ + state(instance?: t.StringId): t.GlobalStateImmutable { + const id = instance ?? defaultId; + if (refs.has(id)) return refs.get(id)!; + + const model = Immutable.clonerRef<t.GlobalState>({ tmp: 0 }); + refs.set(id, model); + return model; + }, +} as const; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/u.Props.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/u.Props.ts new file mode 100644 index 0000000000..f0435495d4 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/u/u.Props.ts @@ -0,0 +1,23 @@ +type O = Record<string, unknown>; + +/** + * Helper for passing props between Vue and React. + */ +export const Props = { + encode<P extends O>(props: P): string { + const json = JSON.stringify(props); + const binary = new TextEncoder().encode(json); + return String(binary); + }, + + decode(encoded: string): O { + if (typeof encoded !== 'string') return {}; + try { + const binary = Uint8Array.from(encoded.split(',')); + const json = new TextDecoder().decode(binary); + return JSON.parse(json); + } catch (error) { + return {}; + } + }, +} as const; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.NotFound.tsx b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.NotFound.tsx new file mode 100644 index 0000000000..50ea6f0bdd --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.NotFound.tsx @@ -0,0 +1,21 @@ +import { css } from '@sys/ui-css'; +import React from 'react'; + +export type NotFoundProps = {}; + +export const NotFound: React.FC<NotFoundProps> = (props) => { + const {} = props; + + const styles = { + base: css({ + backgroundColor: 'rgba(255, 0, 0, 0.1)' /* RED */, + padding: 10, + }), + }; + + return ( + <div className={styles.base.class}> + <div>{`š· Component Not Found`}</div> + </div> + ); +}; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.components.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.components.ts new file mode 100644 index 0000000000..32f727ed08 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.components.ts @@ -0,0 +1,26 @@ +import React from 'react'; + +/** + * Component factory lookup. + */ +export async function lookup(kind: string): Promise<React.FC | undefined> { + if (kind === 'sys/tmp/ui:Foo') { + const { Foo } = await import('@sys/tmp/ui'); + return Foo; + } + + const players = ['ConceptPlayer', 'VideoPlayer', 'Panel']; + + if (players.includes(kind)) { + const { Player } = await import('@sys/ui-react-components'); + if (kind === 'ConceptPlayer') return Player.Concept.View; + if (kind === 'VideoPlayer') return Player.Video.View; + } + + if (kind === 'Panel') { + const { Panel } = await import('@sys/ui-react-components'); + return Panel; + } + + return; // NB: no-match. +} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.setup.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.setup.ts new file mode 100644 index 0000000000..97f72617d8 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.setup.ts @@ -0,0 +1,30 @@ +import React from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { onBeforeUnmount, onMounted, ref as vueRef, type Ref } from 'vue'; +import { NotFound } from './React.NotFound.tsx'; + +type O = Record<string, unknown>; +export const ref = () => vueRef<HTMLElement | undefined>(); + +/** + * Setup a <react-in-vue> wrapper component. + */ +export function setup<P extends O>( + refRoot: Ref<HTMLElement | undefined>, + props: P, + getComponent: () => Promise<React.FC<P> | undefined>, +) { + type C = React.FC<P>; + let root: Root | undefined; + + onMounted(async () => { + if (refRoot.value) { + const Component = ((await getComponent()) || NotFound) as C; + const el = React.createElement(Component, props); + root = createRoot(refRoot.value); + root.render(el); + } + }); + + onBeforeUnmount(() => root?.unmount()); +} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.vue b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.vue new file mode 100644 index 0000000000..88efa40c5d --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/React.vue @@ -0,0 +1,19 @@ +<template> + <div ref="root"></div> +</template> + +<script setup lang="ts"> +import { Props } from '../u/u.Props'; +import { lookup } from './React.components'; +import { ref, setup } from './React.setup'; + +type Base64EncodedJsonString = string; +type InputProps = { kind: string; props?: Base64EncodedJsonString }; + +const root = ref(); +const input = defineProps<InputProps>(); +const props = Props.decode(input.props ?? ''); +setup(root, props, async () => lookup(input.kind)); +</script> + +<style scoped></style> diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/mod.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/mod.ts new file mode 100644 index 0000000000..ae254fc0d0 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.sys/ui/mod.ts @@ -0,0 +1,6 @@ +import type { EnhanceAppContext } from 'vitepress'; +import React from './React.vue'; + +export function registerComponents(ctx: EnhanceAppContext) { + ctx.app.component('React', React); +} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.alias.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.alias.ts deleted file mode 100644 index 5f2a833357..0000000000 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.alias.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Alias } from 'vite'; - -import { DenoDeps } from '@sys/driver-deno/runtime'; -import { ViteConfig } from '@sys/driver-vite'; -import { Err, Path, R } from '@sys/std'; - -/** - * Generate "import" statement alias map for: - * - * - jsr:@sys System modules within the workspace. - * - npm:<deps> The upstream dependencies imported from the NPM registry. - */ -export async function getAliases() { - const deps = (await loadDeps('./.sys/deps.yaml')).deps; - const depsSys = (await loadDeps('./.sys/deps.sys.yaml')).deps; - - const isSys = (name: string) => name.startsWith('@sys/'); - const ws = await ViteConfig.workspace(); - const a = depsSys.map((m) => m.name).filter(isSys); - const b = ws.modules.items.map((m) => m.name).filter(isSys); - const isSystemMonorepo = a.every((item) => b.includes(item)); - if (isSystemMonorepo) return ws.aliases; - - const modules = [...deps, ...depsSys]; - const aliases = modules - .filter((m) => !!m.registry) - .filter((m) => m.version !== '0.0.0') - .map((m) => ViteConfig.alias(m.registry, m.name)); - - const unique = R.uniqBy((item: Alias) => `${item.replacement}:${item.find.toString()}`); - return unique(aliases); -} - -/** - * Helpers - */ -async function loadDeps(path: string) { - const errors = Err.errors(); - path = Path.resolve(import.meta.dirname ?? '', '..', path); - - const res = await DenoDeps.from(path); - if (res.error || !res.data?.deps) { - const err = `Failed to load system dependencies from: ${path}`; - console.warn(err, { cause: res.error }); - errors.push(err); - } - - return { - deps: (res.data?.deps ?? []).map((d) => d.module), - error: errors.toError(), - } as const; -} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.markdown.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.markdown.ts index 4952b6a5ab..2eddaf7fde 100644 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.markdown.ts +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.markdown.ts @@ -1,42 +1,34 @@ import type { MarkdownRenderer } from 'vitepress'; import { parse } from 'yaml'; +import { Props } from '../.sys/mod.ts'; export const markdown = { config(md: MarkdownRenderer) { const rules = md.renderer.rules; - // Store the original fence rule. - const defaultFence = - rules.fence || - function (tokens, idx, options, _env, self) { - return self.renderToken(tokens, idx, options); - }; + // NB: Store the original fence rule. + const originalFenceRule = + rules.fence || ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options)); // Override the fence rule looking for YAML structures. rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx]; + const renderOriginal = () => originalFenceRule(tokens, idx, options, env, self); if (token.info.trim() === 'yaml') { const yaml = parse(token.content) || undefined; - if (yaml?.component === 'Video') { - const defaultHtml = defaultFence(tokens, idx, options, env, self); - const src = yaml.src || ''; - let html = `<Video src="${src}"/>`; - if (yaml.debug) html = `${html}\n${defaultHtml}`; - return html; - } + const formatHtml = (html: string) => (yaml.debug ? `${html}\n${renderOriginal()}` : html); - if (yaml?.component === 'ConceptPlayer') { - const defaultHtml = defaultFence(tokens, idx, options, env, self); - const src = yaml.video || ''; - let html = `<Video src="${src}"/>`; - if (yaml.debug) html = `${html}\n${defaultHtml}`; - return html; + if (typeof yaml?.component === 'string') { + const data = { ...yaml }; + delete data.component; + const html = `<React kind="${yaml.component}" props="${Props.encode(data)}" />`; + return formatHtml(html); } } - // No overriden matches found, return default rendering. - return defaultFence(tokens, idx, options, env, self); + // No match found, return default. + return renderOriginal(); }; }, }; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.ts index 510baefefb..58cd8feea0 100644 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.ts +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/config.ts @@ -1,21 +1,31 @@ import type { ConfigEnv } from 'vite'; +import { ViteConfig } from '@sys/driver-vite'; import { defineConfig } from 'vitepress'; + import { Config } from '../src/config.ts'; import { sidebar } from '../src/nav.ts'; -import { getAliases } from './config.alias.ts'; import { markdown } from './config.markdown.ts'; export default async (env: ConfigEnv) => { const { title, description } = Config; - const alias = (await getAliases()) as any; // NB: type-hack ("vitepress" vs. "vite" fighting). + const ws = await ViteConfig.workspace(); + const alias = ws.aliases; return defineConfig({ title, description, - srcDir: '<SRC_DIR>', + base: '/', + srcDir: './docs', markdown, - themeConfig: { sidebar, search: { provider: 'local' } }, - vite: { resolve: { alias }, plugins: [] }, + themeConfig: { + sidebar, + // search: { provider: 'local' }, + }, + appearance: false, // NB: "light/dark" mode switch. + vite: { + plugins: [], + resolve: { alias }, + }, }); }; diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/Layout.vue b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/Layout.vue new file mode 100644 index 0000000000..c9fdecc1bc --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/Layout.vue @@ -0,0 +1,29 @@ +<template> + <Layout> + <template v-for="slot in slots" #[slot]> + <React kind="Panel" /> + </template> + </Layout> +</template> + +<script setup> +import DefaultTheme from 'vitepress/theme'; +const { Layout } = DefaultTheme; + +/** + * Docs: https://vitepress.dev/guide/extending-default-theme#layout-slots + */ +const slots = [ + 'nav-bar-title-before', + 'nav-bar-title-after', + 'nav-bar-content-before', + 'nav-bar-content-after', + 'nav-screen-content-before', + 'nav-screen-content-after', + 'sidebar-nav-before', + 'sidebar-nav-after', + 'aside-top', + 'aside-outline-before', + 'aside-outline-after', +]; +</script> diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/index.ts b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/index.ts index 25c9325c16..bd91712324 100644 --- a/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/index.ts +++ b/code/sys.driver/driver-vitepress/src/-tmpl/.vitepress/theme/index.ts @@ -1,10 +1,12 @@ import type { Theme as ThemeType } from 'vitepress'; import DefaultTheme from 'vitepress/theme'; -import { registerComponents as registerSystemComponents } from '../../.sys/components/index.ts'; +import { registerComponents as registerSystemComponents } from '../../.sys/ui/mod.ts'; +import Layout from './Layout.vue'; export const Theme: ThemeType = { extends: DefaultTheme, + Layout, enhanceApp(ctx) { registerSystemComponents(ctx); }, diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/deno.json b/code/sys.driver/driver-vitepress/src/-tmpl/deno.json index 80f651cc2c..314dc467d5 100644 --- a/code/sys.driver/driver-vitepress/src/-tmpl/deno.json +++ b/code/sys.driver/driver-vitepress/src/-tmpl/deno.json @@ -1,14 +1,13 @@ { "version": "0.0.0", "tasks": { - "dev": "deno run -RWNE --allow-run --allow-ffi <ENTRY> --cmd=dev", - "build": "deno run -RWE --allow-run --allow-ffi <ENTRY> --cmd=build", - "serve": "deno run -RNE --allow-run --allow-ffi <ENTRY> --cmd=serve", - "clean": "deno run -RWE --allow-ffi <ENTRY> --cmd=clean", - "upgrade": "deno run -RWNE --allow-run --allow-ffi <ENTRY> --cmd=upgrade", - "backup": "deno run -RWE --allow-run --allow-ffi <ENTRY> --cmd=backup", - "help": "deno run -RE --allow-ffi <ENTRY> --cmd=help", - "sys": "deno run -RWNE <ENTRY_SYS>" + "dev": "deno run -RWNE --allow-run --allow-ffi <ENTRY> --cmd=dev", + "build": "deno run -RWE --allow-run --allow-ffi <ENTRY> --cmd=build", + "serve": "deno run -RNE --allow-run --allow-ffi <ENTRY> --cmd=serve", + "clean": "deno run -RWE --allow-ffi <ENTRY> --cmd=clean", + "upgrade": "deno run -RWNE --allow-run --allow-ffi <ENTRY> --cmd=upgrade", + "backup": "deno run -RWE --allow-run --allow-ffi <ENTRY> --cmd=backup", + "help": "deno run -RE --allow-ffi <ENTRY> --cmd=help" }, "compilerOptions": { "strict": true, @@ -18,9 +17,5 @@ }, "workspace": [], "nodeModulesDir": "auto", - "imports": { - "@sys/driver-deno": "<DRIVER_DENO>", - "@sys/driver-vite": "<DRIVER_VITE>", - "@sys/driver-vitepress": "<DRIVER_VITEPRESS>" - } + "importMap": "./imports.json" } diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/docs/index.md b/code/sys.driver/driver-vitepress/src/-tmpl/docs/index.md index 59a89e4200..939a4ab1ce 100644 --- a/code/sys.driver/driver-vitepress/src/-tmpl/docs/index.md +++ b/code/sys.driver/driver-vitepress/src/-tmpl/docs/index.md @@ -19,11 +19,15 @@ notes: | debug: true component: ConceptPlayer video: vimeo/727951677 +thumbnails: true timestamps: + '00:00:00.000': + image: /images/volcano.jpg '00:03:58.215': image: https://wrpcd.net/cdn-cgi/imagedelivery/BXluQx4ige9GuW0Ia56BHw/28f5b7ed-67d1-419d-8db0-d95ae90e8100/rectcontain3 ``` + ```yaml diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/docs/public/images/volcano.jpg b/code/sys.driver/driver-vitepress/src/-tmpl/docs/public/images/volcano.jpg new file mode 100644 index 0000000000..ab0cef116f Binary files /dev/null and b/code/sys.driver/driver-vitepress/src/-tmpl/docs/public/images/volcano.jpg differ diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/imports.json b/code/sys.driver/driver-vitepress/src/-tmpl/imports.json new file mode 100644 index 0000000000..ee0fdf438d --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/-tmpl/imports.json @@ -0,0 +1,9 @@ +{ + "imports": { + "@sys/driver-deno": "jsr:@sys/driver-deno@0.0.97", + "@sys/driver-vite": "jsr:@sys/driver-vite@0.0.137", + "@sys/driver-vitepress": "jsr:@sys/driver-vitepress@0.0.302", + "@deno/vite-plugin": "npm:@deno/vite-plugin@1.0.4", + "@vitejs/plugin-react-swc": "npm:@vitejs/plugin-react-swc@3.8.0" + } +} diff --git a/code/sys.driver/driver-vitepress/src/-tmpl/package.json b/code/sys.driver/driver-vitepress/src/-tmpl/package.json index b0cd38d014..96831fd6f2 100644 --- a/code/sys.driver/driver-vitepress/src/-tmpl/package.json +++ b/code/sys.driver/driver-vitepress/src/-tmpl/package.json @@ -1,17 +1,18 @@ { "dependencies": { - "@vidstack/react": "", - "@sys/std": "npm:@jsr/sys__std", - "@sys/tmp": "npm:@jsr/sys__tmp", - "@sys/ui-css": "npm:@jsr/sys__ui-css", - "react": "", - "react-dom": "", - "yaml": "" + "@vidstack/react": "npm:@vidstack/react@1.12.12", + "@sys/std": "npm:@jsr/sys__std@0.0.141", + "@sys/tmp": "npm:@jsr/sys__tmp@0.0.110", + "@sys/ui-css": "npm:@jsr/sys__ui-css@0.0.81", + "react": "npm:react@18.3.1", + "react-dom": "npm:react-dom@18.3.1", + "yaml": "npm:yaml@2.7.0" }, "devDependencies": { - "@types/react": "", - "@types/react-dom": "", - "vitepress": "", - "vue": "" + "@types/react": "npm:@types/react@18.3.18", + "@types/react-dom": "npm:@types/react-dom@18.3.5", + "vite": "npm:vite@6.1.1", + "vitepress": "npm:vitepress@1.6.3", + "vue": "npm:vue@3.5.13" } } diff --git a/code/sys.driver/driver-vitepress/src/common/libs.ts b/code/sys.driver/driver-vitepress/src/common/libs.ts index 7530c9bb48..ab907d1152 100644 --- a/code/sys.driver/driver-vitepress/src/common/libs.ts +++ b/code/sys.driver/driver-vitepress/src/common/libs.ts @@ -7,6 +7,7 @@ export { Process } from '@sys/process'; export { Args, Date, Err, Is, rx, slug, Str, Time } from '@sys/std'; export { Esm } from '@sys/std/esm'; export { Ignore } from '@sys/std/ignore'; +export { Semver } from '@sys/std/semver/server'; export { HashFmt } from '@sys/crypto/fmt'; export { Hash } from '@sys/crypto/hash'; diff --git a/code/sys.driver/driver-vitepress/src/common/mod.ts b/code/sys.driver/driver-vitepress/src/common/mod.ts index 5bfc83caa3..694597b1d7 100644 --- a/code/sys.driver/driver-vitepress/src/common/mod.ts +++ b/code/sys.driver/driver-vitepress/src/common/mod.ts @@ -1,8 +1,9 @@ export { pkg } from '../pkg.ts'; -export * from './libs.ts'; - export type * as t from './t.ts'; +export * from './libs.ts'; +export * from './u.workspace.ts'; + export const PATHS = { inDir: './', srcDir: './docs', diff --git a/code/sys.driver/driver-vitepress/src/common/t.ts b/code/sys.driver/driver-vitepress/src/common/t.ts index 92b4f5e820..8573e04e35 100644 --- a/code/sys.driver/driver-vitepress/src/common/t.ts +++ b/code/sys.driver/driver-vitepress/src/common/t.ts @@ -1,10 +1,10 @@ export type * from '@sys/driver-vite/t'; export type * from '@sys/types/t'; -export type { DenoFileJson, DenoModuleBackup, Dep } from '@sys/driver-deno/t'; +export type { DenoFileJson, DenoImportMapJson, DenoModuleBackup, Dep } from '@sys/driver-deno/t'; export type { DirSnapshot, FileMap, FsPathFilter } from '@sys/fs/t'; export type { ProcHandle, ProcReadySignalFilter } from '@sys/process/t'; export type { EsmModules, Ignore } from '@sys/std/t'; -export type { Tmpl, TmplCopyHandler, TmplFileOperation, TmplProcessFile } from '@sys/tmpl/t'; +export type { Tmpl, TmplWriteHandler, TmplFileOperation, TmplProcessFile } from '@sys/tmpl/t'; export type * from '../types.ts'; diff --git a/code/sys.driver/driver-vitepress/src/common/u.workspace.ts b/code/sys.driver/driver-vitepress/src/common/u.workspace.ts new file mode 100644 index 0000000000..146c44d576 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/common/u.workspace.ts @@ -0,0 +1,18 @@ +import { DenoDeps, DenoFile, Path, Esm } from './libs.ts'; + +/** + * Find and load the latest workspace data. + */ +export async function getWorkspaceModules() { + const ws = await DenoFile.workspace(); + const deps = (await DenoDeps.from(Path.join(ws.dir, 'deps.yaml'))).data; + const modules = Esm.modules([...(deps?.modules.items ?? []), ...ws.modules.items]); + return { + get ws() { + return ws; + }, + get modules() { + return modules; + }, + } as const; +} diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/-bundle.json b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/-bundle.json index e1ab17915d..5da74aed66 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/-bundle.json +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/-bundle.json @@ -1,26 +1,34 @@ { ".gitignore-": "data:text/plain;base64,bm9kZV9tb2R1bGVzCmRpc3QKLnRtcAoKLnN5cwoudnNjb2RlCi52aXRlcHJlc3MKCi1iYWNrdXAKLWJhY2t1cC8qKgoudml0ZXByZXNzL2NhY2hlLyoqCi52aXRlcHJlc3MvZGlzdC8qKgo=", ".npmrc": "data:text/plain;base64,IyBEb2NzOiBodHRwczovL2pzci5pby9kb2NzL25wbS1jb21wYXRpYmlsaXR5I2luc3RhbGxpbmctYW5kLXVzaW5nLWpzci1wYWNrYWdlcwpAanNyOnJlZ2lzdHJ5PWh0dHBzOi8vbnBtLmpzci5pbwo=", - ".sys/-main.ts": "data:application/typescript;base64,aW1wb3J0ICdqc3I6QHN5cy9kcml2ZXItdml0ZXByZXNzL21haW4nOwo=", - ".sys/components/React.Wrapper.Sample.tsx": "data:application/typescript+jsx;base64,Ly8gQHRzLXR5cGVzPSJAdHlwZXMvcmVhY3QiCmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCc7CgpleHBvcnQgdHlwZSBNeUNvbXBvbmVudFByb3BzID0gewogIGNvdW50PzogbnVtYmVyOwp9OwoKZXhwb3J0IGNvbnN0IE15Q29tcG9uZW50OiBSZWFjdC5GQzxNeUNvbXBvbmVudFByb3BzPiA9IChwcm9wcykgPT4gewogIGNvbnNvbGUubG9nKCdNeUNvbXBvbmVudC5wcm9wczonLCBwcm9wcyk7CiAgcmV0dXJuICgKICAgIDxkaXYgc3R5bGU9e3sgbWFyZ2luVG9wOiA1LCBwYWRkaW5nOiAxMCwgYmFja2dyb3VuZENvbG9yOiAncmdiYSgyNTUsIDAsIDAsIDAuMSknIC8qIFJFRCAqLyB9fT4KICAgICAgPGRpdj5IZWxsbyBmcm9tIFJlYWN0IPCfkYs8L2Rpdj4KICAgIDwvZGl2PgogICk7Cn07Ci8vCg==", - ".sys/components/React.Wrapper.vue": "data:text/plain;base64,Cjx0ZW1wbGF0ZT4KICA8ZGl2IHJlZj0icm9vdCI+PC9kaXY+CjwvdGVtcGxhdGU+Cgo8c2NyaXB0IHNldHVwIGxhbmc9InRzIj4KaW1wb3J0IHsgc2V0dXAsIHJlZiB9IGZyb20gJy4vUmVhY3Quc2V0dXAnOwppbXBvcnQgeyBNeUNvbXBvbmVudCB9IGZyb20gJy4vUmVhY3QuV3JhcHBlci5TYW1wbGUnOwoKY29uc3Qgcm9vdCA9IHJlZigpOwpzZXR1cChyb290LCBNeUNvbXBvbmVudCwgeyBjb3VudDogMTIzNCB9KTsKPC9zY3JpcHQ+Cgo8c3R5bGUgc2NvcGVkPjwvc3R5bGU+Cg==", - ".sys/components/React.setup.ts": "data:application/typescript;base64,CmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCc7CmltcG9ydCBSZWFjdERPTSBmcm9tICdyZWFjdC1kb20vY2xpZW50JzsKaW1wb3J0IHsgb25CZWZvcmVVbm1vdW50LCBvbk1vdW50ZWQsIHJlZiBhcyB2dWVSZWYsIHR5cGUgUmVmIH0gZnJvbSAndnVlJzsKCnR5cGUgTyA9IFJlY29yZDxzdHJpbmcsIHVua25vd24+OwoKZXhwb3J0IGNvbnN0IHJlZiA9ICgpID0+IHZ1ZVJlZjxIVE1MRWxlbWVudCB8IHVuZGVmaW5lZD4oKTsKCi8qKgogKiBTZXR1cCBhIHJlYWN0LWluLXZ1ZSB3cmFwcGVyLgogKi8KZXhwb3J0IGZ1bmN0aW9uIHNldHVwPFAgZXh0ZW5kcyBPPigKICByZWZSb290OiBSZWY8SFRNTEVsZW1lbnQgfCB1bmRlZmluZWQ+LAogIENvbXBvbmVudDogUmVhY3QuRkM8UD4sCiAgcHJvcHM/OiBQLAopIHsKICBsZXQgcm9vdDogUmVhY3RET00uUm9vdCB8IHVuZGVmaW5lZDsKCiAgb25Nb3VudGVkKCgpID0+IHsKICAgIGlmIChyZWZSb290LnZhbHVlKSB7CiAgICAgIGNvbnN0IGVsID0gUmVhY3QuY3JlYXRlRWxlbWVudChDb21wb25lbnQsIHByb3BzKTsKICAgICAgcm9vdCA9IFJlYWN0RE9NLmNyZWF0ZVJvb3QocmVmUm9vdC52YWx1ZSk7CiAgICAgIHJvb3QucmVuZGVyKGVsKTsKICAgIH0KICB9KTsKCiAgb25CZWZvcmVVbm1vdW50KCgpID0+IHsKICAgIHJvb3Q/LnVubW91bnQoKTsKICB9KTsKfQo=", - ".sys/components/Video.tsx": "data:application/typescript+jsx;base64,Ly8gQHRzLXR5cGVzPSJAdHlwZXMvcmVhY3QiCmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCc7CgppbXBvcnQgJ0BzeXMvdG1wL3NhbXBsZS1pbXBvcnRzJzsKaW1wb3J0IHsgRm9vLCBWaWRlb1BsYXllciB9IGZyb20gJ0BzeXMvdG1wL3VpJzsKCmV4cG9ydCBjb25zdCBERUZBVUxUUyA9IHsKICBzcmM6ICd2aW1lby80OTk5MjE1NjEnLCAvLyBUdWJlcy4KfSBhcyBjb25zdDsKCmV4cG9ydCB0eXBlIFZpZGVvUHJvcHMgPSB7CiAgdGl0bGU/OiBzdHJpbmc7CiAgc3JjPzogc3RyaW5nOwp9OwoKLyoqCiAqIENvbXBvbmVudAogKi8KZXhwb3J0IGNvbnN0IFZpZGVvOiBSZWFjdC5GQzxWaWRlb1Byb3BzPiA9IChwcm9wczogVmlkZW9Qcm9wcykgPT4gewogIGNvbnN0IHNyYyA9IHByb3BzLnNyYyB8fCBERUZBVUxUUy5zcmM7CiAgcmV0dXJuICgKICAgIDxkaXY+CiAgICAgIDxGb28gLz4KICAgICAgPFZpZGVvUGxheWVyIC8+CiAgICA8L2Rpdj4KICApOwp9Owo=", - ".sys/components/Video.vue": "data:text/plain;base64,Cjx0ZW1wbGF0ZT4KICA8ZGl2IHJlZj0icm9vdCIgY2xhc3M9InJvb3QiPjwvZGl2Pgo8L3RlbXBsYXRlPgoKPHNjcmlwdCBzZXR1cCBsYW5nPSJ0cyI+CmltcG9ydCB7IHNldHVwLCByZWYgfSBmcm9tICcuL1JlYWN0LnNldHVwJzsKaW1wb3J0IHsgVmlkZW8gfSBmcm9tICcuL1ZpZGVvJzsKCnR5cGUgVmlkZW9Qcm9wcyA9IHsgdGl0bGU/OiBzdHJpbmc7IHNyYz86IHN0cmluZyB9Owpjb25zdCByb290ID0gcmVmKCk7CmNvbnN0IHByb3BzID0gZGVmaW5lUHJvcHM8VmlkZW9Qcm9wcz4oKTsKc2V0dXAocm9vdCwgVmlkZW8sIHByb3BzKTsKPC9zY3JpcHQ+Cgo8c3R5bGUgc2NvcGVkPgoucm9vdCB7CiAgcGFkZGluZy1ib3R0b206IDEwcHg7Cn0KPC9zdHlsZT4K", - ".sys/components/index.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBFbmhhbmNlQXBwQ29udGV4dCB9IGZyb20gJ3ZpdGVwcmVzcyc7CgppbXBvcnQgUmVhY3RXcmFwcGVyIGZyb20gJy4vUmVhY3QuV3JhcHBlci52dWUnOwppbXBvcnQgVmlkZW8gZnJvbSAnLi9WaWRlby52dWUnOwoKZXhwb3J0IGZ1bmN0aW9uIHJlZ2lzdGVyQ29tcG9uZW50cyhjdHg6IEVuaGFuY2VBcHBDb250ZXh0KSB7CiAgY3R4LmFwcC5jb21wb25lbnQoJ1ZpZGVvJywgVmlkZW8pOwogIGN0eC5hcHAuY29tcG9uZW50KCdSZWFjdFdyYXBwZXInLCBSZWFjdFdyYXBwZXIpOwp9Cg==", - ".sys/deps.sys.yaml": "data:text/plain;base64,ZGVuby5qc29uOgogIC0gaW1wb3J0OiBqc3I6QHN5cy90eXBlc0AwLjAuODIKICAtIGltcG9ydDoganNyOkBzeXMvc3RkQDAuMC4xMzEKICAtIGltcG9ydDoganNyOkBzeXMvY29sb3JAMC4wLjM1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3Rlc3RpbmdAMC4wLjc1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2ZzQDAuMC43OQogIC0gaW1wb3J0OiBqc3I6QHN5cy9jbGlAMC4wLjY0CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3Byb2Nlc3NAMC4wLjY1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2NyeXB0b0AwLjAuNjQKICAtIGltcG9ydDoganNyOkBzeXMvaHR0cEAwLjAuNDYKICAtIGltcG9ydDoganNyOkBzeXMvdGV4dEAwLjAuNzMKICAtIGltcG9ydDoganNyOkBzeXMvdG1wbEAwLjAuNzkKICAtIGltcG9ydDoganNyOkBzeXMvY21kQDAuMC44MAogIC0gaW1wb3J0OiBqc3I6QHN5cy9qc3JAMC4wLjQ1CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3VpLWNzc0AwLjAuNzAKICAtIGltcG9ydDoganNyOkBzeXMvdWktZG9tQDAuMC43NwogIC0gaW1wb3J0OiBqc3I6QHN5cy91aS1yZWFjdEAwLjAuODQKICAtIGltcG9ydDoganNyOkBzeXMvdWktcmVhY3QtZGV2aGFybmVzc0AwLjAuODIKICAtIGltcG9ydDoganNyOkBzeXMvdWktcmVhY3QtY29tcG9uZW50c0AwLjAuMzgKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLWF1dG9tZXJnZUAwLjAuODQKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLWRlbm9AMC4wLjg2CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2RyaXZlci1pbW1lckAwLjAuODYKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLW9ic2lkaWFuQDAuMC43MwogIC0gaW1wb3J0OiBqc3I6QHN5cy9kcml2ZXItb2xsYW1hQDAuMC40MAogIC0gaW1wb3J0OiBqc3I6QHN5cy9kcml2ZXItb3JiaXRlckAwLjAuNjMKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLXF1aWxpYnJpdW1AMC4wLjc2CiAgLSBpbXBvcnQ6IGpzcjpAc3lzL2RyaXZlci12aXRlQDAuMC4xMjEKICAtIGltcG9ydDoganNyOkBzeXMvZHJpdmVyLXZpdGVwcmVzc0AwLjAuMjg0LWFscGhhLjIKICAtIGltcG9ydDoganNyOkBzeXMvc3lzQDAuMC41NQogIC0gaW1wb3J0OiBqc3I6QHN5cy9tYWluQDAuMC41NwogIC0gaW1wb3J0OiBqc3I6QHRkYi9hcGlAMC4wLjcyCiAgLSBpbXBvcnQ6IGpzcjpAdGRiL3NsY0AwLjAuNjgKICAtIGltcG9ydDoganNyOkB0ZGIvdG1wQDAuMC43MgogIC0gaW1wb3J0OiBqc3I6QHN5cy9uYW1lQDAuMC4wCiAgLSBpbXBvcnQ6IGpzcjpAc3lzL3RtcEAwLjAuOTUKcGFja2FnZS5qc29uOiBbXQo=", - ".sys/deps.yaml": "data:text/plain;base64,IwojICBTeXN0ZW0gRGVwZW5kZW5jaWVzICgiaW1wb3J0cyIpCiMKIyAgICAgICAgLi/wn5KmCiMgICAgICAgICAgfCAgICAgICAgICAgIGRlbm8uanNvbgojICAgICAgICAgIHwod3JpdGUpICDihpIgIGRlbm8uaW1wb3J0cy5qc29uCiMgICAgICAgICAgfCh3cml0ZSkgIOKGkiAgcGFja2FnZS5qc29uCiMKIyAgVGhpcyBpcyB0aGUgInNpbmdsZS1zb3VyY2Utb2YtdHJ1dGgiIHdpdGggcmVnYXJkcyB0byBkZXBlbmRlbmNpZXMgYW5kIHZlcnNpb25pbmcuCiMgIEltcG9ydCBtYXBzIChpbiB0aGUgYGRlbm8uanNvbmAgYW5kIGBwYWNrYWdlLmpzb25gIGZpbGVzKSBhcmUgYXV0by1nZW5lcmF0ZWQKIyAgZnJvbSB0aGlzIGNvbmZpZyBkZWZpbml0aW9uLgojCiMgIEFsc28sIGFzIGEgcHJvZ3JhbW1hdGljIEFQSSwgb3RoZXIgZG93bnN0cmVhbSBkZXBlbmRlbmNpZXMKIyAgKHN1Y2ggYXMgdGVtcGxhdGUgZ2VuZXJhdG9ycywgc2VlIGBAc3lzL3RtcGxgKSB1c2UgdGhpcyBkZWZpbml0aW9uCiMgIGZpbGUgdG8gY2FsY3VsYXRlIHRoZSAibGF0ZXN0IiB2ZXJzaW9ucyB0byBpbmplY3QgaW50bywgc2F5LAojICBhIGBwYWNrYWdlLmpzb25gIGZpbGUgZm9yIGEgc2NhZmZvbGRlZCBwcm9qZWN0LgojCgpncm91cHM6CiAgc3RkL2Rlbm86CiAgICAjIERlbm8gc3RhbmRhcmQgbGlicyAoInN0ZCIpLgogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL2FzeW5jQDEuMC4xMAogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL2RhdGV0aW1lQDAuMjI1LjMKICAgIC0gaW1wb3J0OiBqc3I6QHN0ZC9kb3RlbnZAMC4yMjUuMwogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL2VuY29kaW5nQDEuMC43CiAgICAtIGltcG9ydDoganNyOkBzdGQvZnNAMS4wLjEzCiAgICAtIGltcG9ydDoganNyOkBzdGQvcGF0aEAxLjAuOAogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL3NlbXZlckAxLjAuNAogICAgLSBpbXBvcnQ6IGpzcjpAc3RkL3Rlc3RpbmdAMS4wLjkKICAgIC0gaW1wb3J0OiBqc3I6QHN0ZC91dWlkQDEuMC40CgogIGF1dG9tZXJnZToKICAgICMgaHR0cHM6Ly9hdXRvbWVyZ2Uub3JnCiAgICAtIGltcG9ydDogbnBtOkBhdXRvbWVyZ2UvYXV0b21lcmdlQDIuMi44CiAgICAtIGltcG9ydDogbnBtOkBhdXRvbWVyZ2UvYXV0b21lcmdlLXJlcG9AMS4yLjEKICAgIC0gaW1wb3J0OiBucG06QGF1dG9tZXJnZS9hdXRvbWVyZ2UtcmVwby1uZXR3b3JrLWJyb2FkY2FzdGNoYW5uZWxAMS4yLjEKICAgIC0gaW1wb3J0OiBucG06QGF1dG9tZXJnZS9hdXRvbWVyZ2UtcmVwby1zdG9yYWdlLWluZGV4ZWRkYkAxLjIuMQogICAgLSBpbXBvcnQ6IG5wbTpAYXV0b21lcmdlL2F1dG9tZXJnZS1yZXBvLXN0b3JhZ2Utbm9kZWZzQDEuMi4xCiAgICAtIGltcG9ydDogbnBtOkBvbnNldHNvZnR3YXJlL2F1dG9tZXJnZS1wYXRjaGVyQDAuMTQuMAoKICBjcnlwdG86CiAgICAtIGltcG9ydDogbnBtOkBub2JsZS9oYXNoZXNAMS43LjEKICAgICAgd2lsZGNhcmQ6IHRydWUKCiAgYnVpbGQvdG9vbHM6CiAgICAtIGltcG9ydDogbnBtOkB2aXRlanMvcGx1Z2luLXJlYWN0LXN3Y0AzLjguMAogICAgLSBpbXBvcnQ6IG5wbTpyb2xsdXBANC4zNC44CiAgICAtIGltcG9ydDogbnBtOnZpdGVANi4xLjEKICAgIC0gaW1wb3J0OiBucG06dml0ZS1wbHVnaW4td2FzbUAzLjQuMQoKICB1aS9yZWFjdDoKICAgIC0gaW1wb3J0OiBucG06QHR5cGVzL3JlYWN0QDE4LjMuMTgKICAgIC0gaW1wb3J0OiBucG06QHR5cGVzL3JlYWN0LWRvbUAxOC4zLjUKICAgIC0gaW1wb3J0OiBucG06cmVhY3RAMTguMy4xCiAgICAtIGltcG9ydDogbnBtOnJlYWN0LWRvbUAxOC4zLjEKCmRlbm8uanNvbjoKICAtIGdyb3VwOiBzdGQvZGVubwogIC0gZ3JvdXA6IGNyeXB0bwogIC0gZ3JvdXA6IGF1dG9tZXJnZQoKICAjIENMSSB0b29scwogIC0gaW1wb3J0OiBqc3I6QGNsaWZmeS9rZXlwcmVzc0AxLjAuMC1yYy43CiAgLSBpbXBvcnQ6IGpzcjpAY2xpZmZ5L3Byb21wdEAxLjAuMC1yYy43CiAgLSBpbXBvcnQ6IGpzcjpAY2xpZmZ5L3RhYmxlQDEuMC4wLXJjLjcKCiAgIyBTdW5kcnk6IE5QTQogIC0gaW1wb3J0OiBucG06QHR5cGVzL2RpZmZANy4wLjEKICAtIGltcG9ydDogbnBtOmNoYWlANQogIC0gaW1wb3J0OiBucG06YXBwcm94LXN0cmluZy1tYXRjaEAyCiAgLSBpbXBvcnQ6IG5wbTpkYXRlLWZuc0A0CiAgLSBpbXBvcnQ6IG5wbTpzdWJob3N0aW5nQDAuMS4wLWFscGhhLjEKICAtIGltcG9ydDogbnBtOmRpZmZANwogIC0gaW1wb3J0OiBucG06ZmFzdC1qc29uLXBhdGNoQDMuMS4xCiAgLSBpbXBvcnQ6IG5wbTpmYWtlLWluZGV4ZWRkYkA2LjAuMAogIC0gaW1wb3J0OiBucG06aGFwcHktZG9tQDE3LjEuMwogIC0gaW1wb3J0OiBucG06aGFzaC1pdEA2LjAuMAogIC0gaW1wb3J0OiBucG06aWdub3JlQDcKICAtIGltcG9ydDogbnBtOmltbWVyQDEwCiAgLSBpbXBvcnQ6IG5wbTpvcmFAOC4yLjAKICAtIGltcG9ydDogbnBtOm9sbGFtYUAwLjUuMTMKICAtIGltcG9ydDogbnBtOnByZXR0eS1ieXRlc0A2LjEuMQogIC0gaW1wb3J0OiBucG06cmFtZGFAMC4zMC4xCiAgLSBpbXBvcnQ6IG5wbTpyYW1iZGFAOS40LjIKICAtIGltcG9ydDogbnBtOnJ4anNANy44LjIKICAtIGltcG9ydDogbnBtOnN0cmlwLWFuc2lANwogIC0gaW1wb3J0OiBucG06c3ViaG9zdGluZ0AwLjEuMC1hbHBoYS4xCiAgLSBpbXBvcnQ6IG5wbTp0aW55Y29sb3IyQDEuNi4wCiAgLSBpbXBvcnQ6IG5wbTp0cy1lc3NlbnRpYWxzQDEwLjAuNAogIC0gaW1wb3J0OiBucG06dmFsaWJvdEAxLjAuMC1yYy4xCiAgLSBpbXBvcnQ6IG5wbTp5YW1sQDIuNy4wCgogICMgQnJvd3NlcgogIC0gaW1wb3J0OiBucG06Y3NzdHlwZUAzCiAgLSBpbXBvcnQ6IG5wbTp1YS1wYXJzZXItanNAMi4wLjIKCiAgIyBVSQogIC0gaW1wb3J0OiBucG06cmVhY3QtZXJyb3ItYm91bmRhcnlANQogIC0gaW1wb3J0OiBucG06cmVhY3QtaW5zcGVjdG9yQDYKICAtIGltcG9ydDogbnBtOnJlYWN0LXNwaW5uZXJzQDAuMTUuMAoKcGFja2FnZS5qc29uOgogIC0gZ3JvdXA6IHN0ZC9kZW5vCiAgLSBncm91cDogY3J5cHRvCiAgLSBncm91cDogYnVpbGQvdG9vbHMKICAgIGRldjogdHJ1ZQoKICAtIGltcG9ydDogbnBtOmhvbm9ANC43LjIKCiAgIyBVSQogIC0gZ3JvdXA6IHVpL3JlYWN0CiAgLSBpbXBvcnQ6IG5wbTpyZWFjdC1pY29uc0A1LjUuMAogIC0gaW1wb3J0OiBucG06QHZpZHN0YWNrL3JlYWN0QDEuMTIuMTIKCiAgIyBVSTpGcmFtZXdvcmtzCiAgLSBpbXBvcnQ6IG5wbTp2aXRlcHJlc3NAMS42LjMKICAtIGltcG9ydDogbnBtOnZ1ZUAzLjUuMTMK", - ".vitepress/config.alias.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBBbGlhcyB9IGZyb20gJ3ZpdGUnOwoKaW1wb3J0IHsgRGVub0RlcHMgfSBmcm9tICdAc3lzL2RyaXZlci1kZW5vL3J1bnRpbWUnOwppbXBvcnQgeyBWaXRlQ29uZmlnIH0gZnJvbSAnQHN5cy9kcml2ZXItdml0ZSc7CmltcG9ydCB7IEVyciwgUGF0aCwgUiB9IGZyb20gJ0BzeXMvc3RkJzsKCi8qKgogKiBHZW5lcmF0ZSAiaW1wb3J0IiBzdGF0ZW1lbnQgYWxpYXMgbWFwIGZvcjoKICoKICogICAgLSBqc3I6QHN5cyAgICAgICAgIFN5c3RlbSBtb2R1bGVzIHdpdGhpbiB0aGUgd29ya3NwYWNlLgogKiAgICAtIG5wbTo8ZGVwcz4gICAgICAgVGhlIHVwc3RyZWFtIGRlcGVuZGVuY2llcyBpbXBvcnRlZCBmcm9tIHRoZSBOUE0gcmVnaXN0cnkuCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gZ2V0QWxpYXNlcygpIHsKICBjb25zdCBkZXBzID0gKGF3YWl0IGxvYWREZXBzKCcuLy5zeXMvZGVwcy55YW1sJykpLmRlcHM7CiAgY29uc3QgZGVwc1N5cyA9IChhd2FpdCBsb2FkRGVwcygnLi8uc3lzL2RlcHMuc3lzLnlhbWwnKSkuZGVwczsKCiAgY29uc3QgaXNTeXMgPSAobmFtZTogc3RyaW5nKSA9PiBuYW1lLnN0YXJ0c1dpdGgoJ0BzeXMvJyk7CiAgY29uc3Qgd3MgPSBhd2FpdCBWaXRlQ29uZmlnLndvcmtzcGFjZSgpOwogIGNvbnN0IGEgPSBkZXBzU3lzLm1hcCgobSkgPT4gbS5uYW1lKS5maWx0ZXIoaXNTeXMpOwogIGNvbnN0IGIgPSB3cy5tb2R1bGVzLml0ZW1zLm1hcCgobSkgPT4gbS5uYW1lKS5maWx0ZXIoaXNTeXMpOwogIGNvbnN0IGlzU3lzdGVtTW9ub3JlcG8gPSBhLmV2ZXJ5KChpdGVtKSA9PiBiLmluY2x1ZGVzKGl0ZW0pKTsKICBpZiAoaXNTeXN0ZW1Nb25vcmVwbykgcmV0dXJuIHdzLmFsaWFzZXM7CgogIGNvbnN0IG1vZHVsZXMgPSBbLi4uZGVwcywgLi4uZGVwc1N5c107CiAgY29uc3QgYWxpYXNlcyA9IG1vZHVsZXMKICAgIC5maWx0ZXIoKG0pID0+ICEhbS5yZWdpc3RyeSkKICAgIC5maWx0ZXIoKG0pID0+IG0udmVyc2lvbiAhPT0gJzAuMC4wJykKICAgIC5tYXAoKG0pID0+IFZpdGVDb25maWcuYWxpYXMobS5yZWdpc3RyeSwgbS5uYW1lKSk7CgogIGNvbnN0IHVuaXF1ZSA9IFIudW5pcUJ5KChpdGVtOiBBbGlhcykgPT4gYCR7aXRlbS5yZXBsYWNlbWVudH06JHtpdGVtLmZpbmQudG9TdHJpbmcoKX1gKTsKICByZXR1cm4gdW5pcXVlKGFsaWFzZXMpOwp9CgovKioKICogSGVscGVycwogKi8KYXN5bmMgZnVuY3Rpb24gbG9hZERlcHMocGF0aDogc3RyaW5nKSB7CiAgY29uc3QgZXJyb3JzID0gRXJyLmVycm9ycygpOwogIHBhdGggPSBQYXRoLnJlc29sdmUoaW1wb3J0Lm1ldGEuZGlybmFtZSA/PyAnJywgJy4uJywgcGF0aCk7CgogIGNvbnN0IHJlcyA9IGF3YWl0IERlbm9EZXBzLmZyb20ocGF0aCk7CiAgaWYgKHJlcy5lcnJvciB8fCAhcmVzLmRhdGE/LmRlcHMpIHsKICAgIGNvbnN0IGVyciA9IGBGYWlsZWQgdG8gbG9hZCBzeXN0ZW0gZGVwZW5kZW5jaWVzIGZyb206ICR7cGF0aH1gOwogICAgY29uc29sZS53YXJuKGVyciwgeyBjYXVzZTogcmVzLmVycm9yIH0pOwogICAgZXJyb3JzLnB1c2goZXJyKTsKICB9CgogIHJldHVybiB7CiAgICBkZXBzOiAocmVzLmRhdGE/LmRlcHMgPz8gW10pLm1hcCgoZCkgPT4gZC5tb2R1bGUpLAogICAgZXJyb3I6IGVycm9ycy50b0Vycm9yKCksCiAgfSBhcyBjb25zdDsKfQo=", - ".vitepress/config.markdown.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBNYXJrZG93blJlbmRlcmVyIH0gZnJvbSAndml0ZXByZXNzJzsKaW1wb3J0IHsgcGFyc2UgfSBmcm9tICd5YW1sJzsKCmV4cG9ydCBjb25zdCBtYXJrZG93biA9IHsKICBjb25maWcobWQ6IE1hcmtkb3duUmVuZGVyZXIpIHsKICAgIGNvbnN0IHJ1bGVzID0gbWQucmVuZGVyZXIucnVsZXM7CgogICAgLy8gU3RvcmUgdGhlIG9yaWdpbmFsIGZlbmNlIHJ1bGUuCiAgICBjb25zdCBkZWZhdWx0RmVuY2UgPQogICAgICBydWxlcy5mZW5jZSB8fAogICAgICBmdW5jdGlvbiAodG9rZW5zLCBpZHgsIG9wdGlvbnMsIF9lbnYsIHNlbGYpIHsKICAgICAgICByZXR1cm4gc2VsZi5yZW5kZXJUb2tlbih0b2tlbnMsIGlkeCwgb3B0aW9ucyk7CiAgICAgIH07CgogICAgLy8gT3ZlcnJpZGUgdGhlIGZlbmNlIHJ1bGUgbG9va2luZyBmb3IgWUFNTCBzdHJ1Y3R1cmVzLgogICAgcnVsZXMuZmVuY2UgPSAodG9rZW5zLCBpZHgsIG9wdGlvbnMsIGVudiwgc2VsZikgPT4gewogICAgICBjb25zdCB0b2tlbiA9IHRva2Vuc1tpZHhdOwoKICAgICAgaWYgKHRva2VuLmluZm8udHJpbSgpID09PSAneWFtbCcpIHsKICAgICAgICBjb25zdCB5YW1sID0gcGFyc2UodG9rZW4uY29udGVudCkgfHwgdW5kZWZpbmVkOwogICAgICAgIGlmICh5YW1sPy5jb21wb25lbnQgPT09ICdWaWRlbycpIHsKICAgICAgICAgIGNvbnN0IGRlZmF1bHRIdG1sID0gZGVmYXVsdEZlbmNlKHRva2VucywgaWR4LCBvcHRpb25zLCBlbnYsIHNlbGYpOwogICAgICAgICAgY29uc3Qgc3JjID0geWFtbC5zcmMgfHwgJyc7CiAgICAgICAgICBsZXQgaHRtbCA9IGA8VmlkZW8gc3JjPSIke3NyY30iLz5gOwogICAgICAgICAgaWYgKHlhbWwuZGVidWcpIGh0bWwgPSBgJHtodG1sfVxuJHtkZWZhdWx0SHRtbH1gOwogICAgICAgICAgcmV0dXJuIGh0bWw7CiAgICAgICAgfQoKICAgICAgICBpZiAoeWFtbD8uY29tcG9uZW50ID09PSAnQ29uY2VwdFBsYXllcicpIHsKICAgICAgICAgIGNvbnN0IGRlZmF1bHRIdG1sID0gZGVmYXVsdEZlbmNlKHRva2VucywgaWR4LCBvcHRpb25zLCBlbnYsIHNlbGYpOwogICAgICAgICAgY29uc3Qgc3JjID0geWFtbC52aWRlbyB8fCAnJzsKICAgICAgICAgIGxldCBodG1sID0gYDxWaWRlbyBzcmM9IiR7c3JjfSIvPmA7CiAgICAgICAgICBpZiAoeWFtbC5kZWJ1ZykgaHRtbCA9IGAke2h0bWx9XG4ke2RlZmF1bHRIdG1sfWA7CiAgICAgICAgICByZXR1cm4gaHRtbDsKICAgICAgICB9CiAgICAgIH0KCiAgICAgIC8vIE5vIG92ZXJyaWRlbiBtYXRjaGVzIGZvdW5kLCByZXR1cm4gZGVmYXVsdCByZW5kZXJpbmcuCiAgICAgIHJldHVybiBkZWZhdWx0RmVuY2UodG9rZW5zLCBpZHgsIG9wdGlvbnMsIGVudiwgc2VsZik7CiAgICB9OwogIH0sCn07Cg==", - ".vitepress/config.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBDb25maWdFbnYgfSBmcm9tICd2aXRlJzsKCmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGVwcmVzcyc7CmltcG9ydCB7IENvbmZpZyB9IGZyb20gJy4uL3NyYy9jb25maWcudHMnOwppbXBvcnQgeyBzaWRlYmFyIH0gZnJvbSAnLi4vc3JjL25hdi50cyc7CmltcG9ydCB7IGdldEFsaWFzZXMgfSBmcm9tICcuL2NvbmZpZy5hbGlhcy50cyc7CmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAnLi9jb25maWcubWFya2Rvd24udHMnOwoKZXhwb3J0IGRlZmF1bHQgYXN5bmMgKGVudjogQ29uZmlnRW52KSA9PiB7CiAgY29uc3QgeyB0aXRsZSwgZGVzY3JpcHRpb24gfSA9IENvbmZpZzsKICBjb25zdCBhbGlhcyA9IChhd2FpdCBnZXRBbGlhc2VzKCkpIGFzIGFueTsgLy8gTkI6IHR5cGUtaGFjayAoInZpdGVwcmVzcyIgdnMuICJ2aXRlIiBmaWdodGluZykuCgogIHJldHVybiBkZWZpbmVDb25maWcoewogICAgdGl0bGUsCiAgICBkZXNjcmlwdGlvbiwKICAgIHNyY0RpcjogJzxTUkNfRElSPicsCiAgICBtYXJrZG93biwKICAgIHRoZW1lQ29uZmlnOiB7IHNpZGViYXIsIHNlYXJjaDogeyBwcm92aWRlcjogJ2xvY2FsJyB9IH0sCiAgICB2aXRlOiB7IHJlc29sdmU6IHsgYWxpYXMgfSwgcGx1Z2luczogW10gfSwKICB9KTsKfTsK", + ".sys/common.ts": "data:application/typescript;base64,ZXhwb3J0ICogZnJvbSAnLi9jb21tb24vbW9kLnRzJzsK", + ".sys/common/libs.ts": "data:application/typescript;base64,ZXhwb3J0IHsgSW1tdXRhYmxlLCBzbHVnIH0gZnJvbSAnQHN5cy9zdGQnOwo=", + ".sys/common/mod.ts": "data:application/typescript;base64,ZXhwb3J0ICogZnJvbSAnLi9saWJzLnRzJzsKZXhwb3J0IHR5cGUgKiBhcyB0IGZyb20gJy4vdC50cyc7Cg==", + ".sys/common/t.ts": "data:application/typescript;base64,ZXhwb3J0IHR5cGUgKiBmcm9tICdAc3lzL3R5cGVzJzsKZXhwb3J0IHR5cGUgKiBmcm9tICcuLi90eXBlcy50cyc7Cg==", + ".sys/mod.ts": "data:application/typescript;base64,ZXhwb3J0IHsgUHJvcHMgfSBmcm9tICcuL3UvdS5Qcm9wcy50cyc7CmV4cG9ydCB7IEdsb2JhbCB9IGZyb20gJy4vdS91Lkdsb2JhbC50cyc7Cg==", + ".sys/types.ts": "data:application/typescript;base64,LyoqCiAqIEBtb2R1bGUKICogU3lzdGVtIHR5cGVzCiAqLwpleHBvcnQgdHlwZSAqIGZyb20gJy4vdS90LnRzJzsK", + ".sys/u/common.ts": "data:application/typescript;base64,ZXhwb3J0ICogZnJvbSAnLi4vY29tbW9uLnRzJzsK", + ".sys/u/t.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyB0IH0gZnJvbSAnLi9jb21tb24udHMnOwoKZXhwb3J0IHR5cGUgR2xvYmFsU3RhdGUgPSB7CiAgdG1wOiBudW1iZXI7Cn07CgovKioKICogSW1tdXRhYmxlIHdyYXBwZXIuCiAqLwp0eXBlIFAgPSB0LlBhdGNoT3BlcmF0aW9uOwpleHBvcnQgdHlwZSBHbG9iYWxTdGF0ZUltbXV0YWJsZSA9IHQuSW1tdXRhYmxlUmVmPHQuR2xvYmFsU3RhdGUsIFAsIEdsb2JhbFN0YXRlRXZlbnRzPjsKCmV4cG9ydCB0eXBlIEdsb2JhbFN0YXRlRXZlbnRzID0gdC5JbW11dGFibGVFdmVudHM8R2xvYmFsU3RhdGUsIFA+OwpleHBvcnQgdHlwZSBHbG9iYWxTdGF0ZUV2ZW50ID0gdC5JbmZlckltbXV0YWJsZUV2ZW50PHQuR2xvYmFsU3RhdGVFdmVudHM+Owo=", + ".sys/u/u.Global.ts": "data:application/typescript;base64,aW1wb3J0IHsgdHlwZSB0LCBJbW11dGFibGUsIHNsdWcgfSBmcm9tICcuL2NvbW1vbi50cyc7Cgpjb25zdCBkZWZhdWx0SWQgPSBgZGVmYXVsdDoke3NsdWcoKX1gOwpjb25zdCByZWZzID0gbmV3IE1hcDxzdHJpbmcsIHQuR2xvYmFsU3RhdGVJbW11dGFibGU+KCk7CgovKioKICogR2xvYmFsIHN0YXRlIGludGVyZmFjZS4KICovCmV4cG9ydCBjb25zdCBHbG9iYWwgPSB7CiAgLyoqCiAgICogUmV0cmlldmUgYW4gaW5zdGFuY2Ugb2YgdGhlIGdsb2JhbCBzdGF0ZSBvYmplY3QuCiAgICoKICAgKiBAcGFyYW0gaW5zdGFuY2UgLSBPcHRpb25hbCB1bmlxdWUgaWRlbnRpZmllciBmb3IgdGhlIGdsb2JhbCBzdGF0ZSBpbnN0YW5jZS4KICAgKiAgICAgICAgICAgICAgICAgICBJZiBub3QgcHJvdmlkZWQsIHRoZSBkZWZhdWx0IGluc3RhbmNlIGlzIHJldHVybmVkLgogICAqICAgICAgICAgICAgICAgICAgIE5vIHBhcmFtIGlzIHRoZSBlcXVpdmFsZW50IG9mIHJldHJpZXZpbmcgdGhlICJzaW5nbHRvbiIgaW5zdGFuY2UuCiAgICoKICAgKiBAcmV0dXJucyBUaGUgZ2xvYmFsIHN0YXRlIGluc3RhbmNlIGNvcnJlc3BvbmRpbmcgdG8gdGhlIHByb3ZpZGVkIGlkZW50aWZpZXIuCiAgICovCiAgc3RhdGUoaW5zdGFuY2U/OiB0LlN0cmluZ0lkKTogdC5HbG9iYWxTdGF0ZUltbXV0YWJsZSB7CiAgICBjb25zdCBpZCA9IGluc3RhbmNlID8/IGRlZmF1bHRJZDsKICAgIGlmIChyZWZzLmhhcyhpZCkpIHJldHVybiByZWZzLmdldChpZCkhOwoKICAgIGNvbnN0IG1vZGVsID0gSW1tdXRhYmxlLmNsb25lclJlZjx0Lkdsb2JhbFN0YXRlPih7IHRtcDogMCB9KTsKICAgIHJlZnMuc2V0KGlkLCBtb2RlbCk7CiAgICByZXR1cm4gbW9kZWw7CiAgfSwKfSBhcyBjb25zdDsK", + ".sys/u/u.Props.ts": "data:application/typescript;base64,dHlwZSBPID0gUmVjb3JkPHN0cmluZywgdW5rbm93bj47CgovKioKICogSGVscGVyIGZvciBwYXNzaW5nIHByb3BzIGJldHdlZW4gVnVlIGFuZCBSZWFjdC4KICovCmV4cG9ydCBjb25zdCBQcm9wcyA9IHsKICBlbmNvZGU8UCBleHRlbmRzIE8+KHByb3BzOiBQKTogc3RyaW5nIHsKICAgIGNvbnN0IGpzb24gPSBKU09OLnN0cmluZ2lmeShwcm9wcyk7CiAgICBjb25zdCBiaW5hcnkgPSBuZXcgVGV4dEVuY29kZXIoKS5lbmNvZGUoanNvbik7CiAgICByZXR1cm4gU3RyaW5nKGJpbmFyeSk7CiAgfSwKCiAgZGVjb2RlKGVuY29kZWQ6IHN0cmluZyk6IE8gewogICAgaWYgKHR5cGVvZiBlbmNvZGVkICE9PSAnc3RyaW5nJykgcmV0dXJuIHt9OwogICAgdHJ5IHsKICAgICAgY29uc3QgYmluYXJ5ID0gVWludDhBcnJheS5mcm9tKGVuY29kZWQuc3BsaXQoJywnKSk7CiAgICAgIGNvbnN0IGpzb24gPSBuZXcgVGV4dERlY29kZXIoKS5kZWNvZGUoYmluYXJ5KTsKICAgICAgcmV0dXJuIEpTT04ucGFyc2UoanNvbik7CiAgICB9IGNhdGNoIChlcnJvcikgewogICAgICByZXR1cm4ge307CiAgICB9CiAgfSwKfSBhcyBjb25zdDsK", + ".sys/ui/React.NotFound.tsx": "data:application/typescript+jsx;base64,aW1wb3J0IHsgY3NzIH0gZnJvbSAnQHN5cy91aS1jc3MnOwppbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnOwoKZXhwb3J0IHR5cGUgTm90Rm91bmRQcm9wcyA9IHt9OwoKZXhwb3J0IGNvbnN0IE5vdEZvdW5kOiBSZWFjdC5GQzxOb3RGb3VuZFByb3BzPiA9IChwcm9wcykgPT4gewogIGNvbnN0IHt9ID0gcHJvcHM7CgogIGNvbnN0IHN0eWxlcyA9IHsKICAgIGJhc2U6IGNzcyh7CiAgICAgIGJhY2tncm91bmRDb2xvcjogJ3JnYmEoMjU1LCAwLCAwLCAwLjEpJyAvKiBSRUQgKi8sCiAgICAgIHBhZGRpbmc6IDEwLAogICAgfSksCiAgfTsKCiAgcmV0dXJuICgKICAgIDxkaXYgY2xhc3NOYW1lPXtzdHlsZXMuYmFzZS5jbGFzc30+CiAgICAgIDxkaXY+e2Dwn5C3IENvbXBvbmVudCBOb3QgRm91bmRgfTwvZGl2PgogICAgPC9kaXY+CiAgKTsKfTsK", + ".sys/ui/React.components.ts": "data:application/typescript;base64,aW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0JzsKCi8qKgogKiBDb21wb25lbnQgZmFjdG9yeSBsb29rdXAuCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gbG9va3VwKGtpbmQ6IHN0cmluZyk6IFByb21pc2U8UmVhY3QuRkMgfCB1bmRlZmluZWQ+IHsKICBpZiAoa2luZCA9PT0gJ3N5cy90bXAvdWk6Rm9vJykgewogICAgY29uc3QgeyBGb28gfSA9IGF3YWl0IGltcG9ydCgnQHN5cy90bXAvdWknKTsKICAgIHJldHVybiBGb287CiAgfQoKICBjb25zdCBwbGF5ZXJzID0gWydDb25jZXB0UGxheWVyJywgJ1ZpZGVvUGxheWVyJywgJ1BhbmVsJ107CgogIGlmIChwbGF5ZXJzLmluY2x1ZGVzKGtpbmQpKSB7CiAgICBjb25zdCB7IFBsYXllciB9ID0gYXdhaXQgaW1wb3J0KCdAc3lzL3VpLXJlYWN0LWNvbXBvbmVudHMnKTsKICAgIGlmIChraW5kID09PSAnQ29uY2VwdFBsYXllcicpIHJldHVybiBQbGF5ZXIuQ29uY2VwdC5WaWV3OwogICAgaWYgKGtpbmQgPT09ICdWaWRlb1BsYXllcicpIHJldHVybiBQbGF5ZXIuVmlkZW8uVmlldzsKICB9CgogIGlmIChraW5kID09PSAnUGFuZWwnKSB7CiAgICBjb25zdCB7IFBhbmVsIH0gPSBhd2FpdCBpbXBvcnQoJ0BzeXMvdWktcmVhY3QtY29tcG9uZW50cycpOwogICAgcmV0dXJuIFBhbmVsOwogIH0KCiAgcmV0dXJuOyAvLyBOQjogbm8tbWF0Y2guCn0K", + ".sys/ui/React.setup.ts": "data:application/typescript;base64,aW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0JzsKaW1wb3J0IHsgY3JlYXRlUm9vdCwgdHlwZSBSb290IH0gZnJvbSAncmVhY3QtZG9tL2NsaWVudCc7CmltcG9ydCB7IG9uQmVmb3JlVW5tb3VudCwgb25Nb3VudGVkLCByZWYgYXMgdnVlUmVmLCB0eXBlIFJlZiB9IGZyb20gJ3Z1ZSc7CmltcG9ydCB7IE5vdEZvdW5kIH0gZnJvbSAnLi9SZWFjdC5Ob3RGb3VuZC50c3gnOwoKdHlwZSBPID0gUmVjb3JkPHN0cmluZywgdW5rbm93bj47CmV4cG9ydCBjb25zdCByZWYgPSAoKSA9PiB2dWVSZWY8SFRNTEVsZW1lbnQgfCB1bmRlZmluZWQ+KCk7CgovKioKICogU2V0dXAgYSA8cmVhY3QtaW4tdnVlPiB3cmFwcGVyIGNvbXBvbmVudC4KICovCmV4cG9ydCBmdW5jdGlvbiBzZXR1cDxQIGV4dGVuZHMgTz4oCiAgcmVmUm9vdDogUmVmPEhUTUxFbGVtZW50IHwgdW5kZWZpbmVkPiwKICBwcm9wczogUCwKICBnZXRDb21wb25lbnQ6ICgpID0+IFByb21pc2U8UmVhY3QuRkM8UD4gfCB1bmRlZmluZWQ+LAopIHsKICB0eXBlIEMgPSBSZWFjdC5GQzxQPjsKICBsZXQgcm9vdDogUm9vdCB8IHVuZGVmaW5lZDsKCiAgb25Nb3VudGVkKGFzeW5jICgpID0+IHsKICAgIGlmIChyZWZSb290LnZhbHVlKSB7CiAgICAgIGNvbnN0IENvbXBvbmVudCA9ICgoYXdhaXQgZ2V0Q29tcG9uZW50KCkpIHx8IE5vdEZvdW5kKSBhcyBDOwogICAgICBjb25zdCBlbCA9IFJlYWN0LmNyZWF0ZUVsZW1lbnQoQ29tcG9uZW50LCBwcm9wcyk7CiAgICAgIHJvb3QgPSBjcmVhdGVSb290KHJlZlJvb3QudmFsdWUpOwogICAgICByb290LnJlbmRlcihlbCk7CiAgICB9CiAgfSk7CgogIG9uQmVmb3JlVW5tb3VudCgoKSA9PiByb290Py51bm1vdW50KCkpOwp9Cg==", + ".sys/ui/React.vue": "data:text/plain;base64,PHRlbXBsYXRlPgogIDxkaXYgcmVmPSJyb290Ij48L2Rpdj4KPC90ZW1wbGF0ZT4KCjxzY3JpcHQgc2V0dXAgbGFuZz0idHMiPgppbXBvcnQgeyBQcm9wcyB9IGZyb20gJy4uL3UvdS5Qcm9wcyc7CmltcG9ydCB7IGxvb2t1cCB9IGZyb20gJy4vUmVhY3QuY29tcG9uZW50cyc7CmltcG9ydCB7IHJlZiwgc2V0dXAgfSBmcm9tICcuL1JlYWN0LnNldHVwJzsKCnR5cGUgQmFzZTY0RW5jb2RlZEpzb25TdHJpbmcgPSBzdHJpbmc7CnR5cGUgSW5wdXRQcm9wcyA9IHsga2luZDogc3RyaW5nOyBwcm9wcz86IEJhc2U2NEVuY29kZWRKc29uU3RyaW5nIH07Cgpjb25zdCByb290ID0gcmVmKCk7CmNvbnN0IGlucHV0ID0gZGVmaW5lUHJvcHM8SW5wdXRQcm9wcz4oKTsKY29uc3QgcHJvcHMgPSBQcm9wcy5kZWNvZGUoaW5wdXQucHJvcHMgPz8gJycpOwpzZXR1cChyb290LCBwcm9wcywgYXN5bmMgKCkgPT4gbG9va3VwKGlucHV0LmtpbmQpKTsKPC9zY3JpcHQ+Cgo8c3R5bGUgc2NvcGVkPjwvc3R5bGU+Cg==", + ".sys/ui/mod.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBFbmhhbmNlQXBwQ29udGV4dCB9IGZyb20gJ3ZpdGVwcmVzcyc7CmltcG9ydCBSZWFjdCBmcm9tICcuL1JlYWN0LnZ1ZSc7CgpleHBvcnQgZnVuY3Rpb24gcmVnaXN0ZXJDb21wb25lbnRzKGN0eDogRW5oYW5jZUFwcENvbnRleHQpIHsKICBjdHguYXBwLmNvbXBvbmVudCgnUmVhY3QnLCBSZWFjdCk7Cn0K", + ".vitepress/config.markdown.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBNYXJrZG93blJlbmRlcmVyIH0gZnJvbSAndml0ZXByZXNzJzsKaW1wb3J0IHsgcGFyc2UgfSBmcm9tICd5YW1sJzsKaW1wb3J0IHsgUHJvcHMgfSBmcm9tICcuLi8uc3lzL21vZC50cyc7CgpleHBvcnQgY29uc3QgbWFya2Rvd24gPSB7CiAgY29uZmlnKG1kOiBNYXJrZG93blJlbmRlcmVyKSB7CiAgICBjb25zdCBydWxlcyA9IG1kLnJlbmRlcmVyLnJ1bGVzOwoKICAgIC8vIE5COiBTdG9yZSB0aGUgb3JpZ2luYWwgZmVuY2UgcnVsZS4KICAgIGNvbnN0IG9yaWdpbmFsRmVuY2VSdWxlID0KICAgICAgcnVsZXMuZmVuY2UgfHwgKCh0b2tlbnMsIGlkeCwgb3B0aW9ucywgZW52LCBzZWxmKSA9PiBzZWxmLnJlbmRlclRva2VuKHRva2VucywgaWR4LCBvcHRpb25zKSk7CgogICAgLy8gT3ZlcnJpZGUgdGhlIGZlbmNlIHJ1bGUgbG9va2luZyBmb3IgWUFNTCBzdHJ1Y3R1cmVzLgogICAgcnVsZXMuZmVuY2UgPSAodG9rZW5zLCBpZHgsIG9wdGlvbnMsIGVudiwgc2VsZikgPT4gewogICAgICBjb25zdCB0b2tlbiA9IHRva2Vuc1tpZHhdOwogICAgICBjb25zdCByZW5kZXJPcmlnaW5hbCA9ICgpID0+IG9yaWdpbmFsRmVuY2VSdWxlKHRva2VucywgaWR4LCBvcHRpb25zLCBlbnYsIHNlbGYpOwoKICAgICAgaWYgKHRva2VuLmluZm8udHJpbSgpID09PSAneWFtbCcpIHsKICAgICAgICBjb25zdCB5YW1sID0gcGFyc2UodG9rZW4uY29udGVudCkgfHwgdW5kZWZpbmVkOwogICAgICAgIGNvbnN0IGZvcm1hdEh0bWwgPSAoaHRtbDogc3RyaW5nKSA9PiAoeWFtbC5kZWJ1ZyA/IGAke2h0bWx9XG4ke3JlbmRlck9yaWdpbmFsKCl9YCA6IGh0bWwpOwoKICAgICAgICBpZiAodHlwZW9mIHlhbWw/LmNvbXBvbmVudCA9PT0gJ3N0cmluZycpIHsKICAgICAgICAgIGNvbnN0IGRhdGEgPSB7IC4uLnlhbWwgfTsKICAgICAgICAgIGRlbGV0ZSBkYXRhLmNvbXBvbmVudDsKICAgICAgICAgIGNvbnN0IGh0bWwgPSBgPFJlYWN0IGtpbmQ9IiR7eWFtbC5jb21wb25lbnR9IiBwcm9wcz0iJHtQcm9wcy5lbmNvZGUoZGF0YSl9IiAvPmA7CiAgICAgICAgICByZXR1cm4gZm9ybWF0SHRtbChodG1sKTsKICAgICAgICB9CiAgICAgIH0KCiAgICAgIC8vIE5vIG1hdGNoIGZvdW5kLCByZXR1cm4gZGVmYXVsdC4KICAgICAgcmV0dXJuIHJlbmRlck9yaWdpbmFsKCk7CiAgICB9OwogIH0sCn07Cg==", + ".vitepress/config.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBDb25maWdFbnYgfSBmcm9tICd2aXRlJzsKCmltcG9ydCB7IFZpdGVDb25maWcgfSBmcm9tICdAc3lzL2RyaXZlci12aXRlJzsKaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZXByZXNzJzsKCmltcG9ydCB7IENvbmZpZyB9IGZyb20gJy4uL3NyYy9jb25maWcudHMnOwppbXBvcnQgeyBzaWRlYmFyIH0gZnJvbSAnLi4vc3JjL25hdi50cyc7CmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAnLi9jb25maWcubWFya2Rvd24udHMnOwoKZXhwb3J0IGRlZmF1bHQgYXN5bmMgKGVudjogQ29uZmlnRW52KSA9PiB7CiAgY29uc3QgeyB0aXRsZSwgZGVzY3JpcHRpb24gfSA9IENvbmZpZzsKICBjb25zdCB3cyA9IGF3YWl0IFZpdGVDb25maWcud29ya3NwYWNlKCk7CiAgY29uc3QgYWxpYXMgPSB3cy5hbGlhc2VzOwoKICByZXR1cm4gZGVmaW5lQ29uZmlnKHsKICAgIHRpdGxlLAogICAgZGVzY3JpcHRpb24sCiAgICBiYXNlOiAnLycsCiAgICBzcmNEaXI6ICcuL2RvY3MnLAogICAgbWFya2Rvd24sCiAgICB0aGVtZUNvbmZpZzogewogICAgICBzaWRlYmFyLAogICAgICAvLyBzZWFyY2g6IHsgcHJvdmlkZXI6ICdsb2NhbCcgfSwKICAgIH0sCiAgICBhcHBlYXJhbmNlOiBmYWxzZSwgLy8gTkI6ICJsaWdodC9kYXJrIiBtb2RlIHN3aXRjaC4KICAgIHZpdGU6IHsKICAgICAgcGx1Z2luczogW10sCiAgICAgIHJlc29sdmU6IHsgYWxpYXMgfSwKICAgIH0sCiAgfSk7Cn07Cg==", + ".vitepress/theme/Layout.vue": "data:text/plain;base64,PHRlbXBsYXRlPgogIDxMYXlvdXQ+CiAgICA8dGVtcGxhdGUgdi1mb3I9InNsb3QgaW4gc2xvdHMiICNbc2xvdF0+CiAgICAgIDxSZWFjdCBraW5kPSJQYW5lbCIgLz4KICAgIDwvdGVtcGxhdGU+CiAgPC9MYXlvdXQ+CjwvdGVtcGxhdGU+Cgo8c2NyaXB0IHNldHVwPgppbXBvcnQgRGVmYXVsdFRoZW1lIGZyb20gJ3ZpdGVwcmVzcy90aGVtZSc7CmNvbnN0IHsgTGF5b3V0IH0gPSBEZWZhdWx0VGhlbWU7CgovKioKICogRG9jczogaHR0cHM6Ly92aXRlcHJlc3MuZGV2L2d1aWRlL2V4dGVuZGluZy1kZWZhdWx0LXRoZW1lI2xheW91dC1zbG90cwogKi8KY29uc3Qgc2xvdHMgPSBbCiAgJ25hdi1iYXItdGl0bGUtYmVmb3JlJywKICAnbmF2LWJhci10aXRsZS1hZnRlcicsCiAgJ25hdi1iYXItY29udGVudC1iZWZvcmUnLAogICduYXYtYmFyLWNvbnRlbnQtYWZ0ZXInLAogICduYXYtc2NyZWVuLWNvbnRlbnQtYmVmb3JlJywKICAnbmF2LXNjcmVlbi1jb250ZW50LWFmdGVyJywKICAnc2lkZWJhci1uYXYtYmVmb3JlJywKICAnc2lkZWJhci1uYXYtYWZ0ZXInLAogICdhc2lkZS10b3AnLAogICdhc2lkZS1vdXRsaW5lLWJlZm9yZScsCiAgJ2FzaWRlLW91dGxpbmUtYWZ0ZXInLApdOwo8L3NjcmlwdD4K", ".vitepress/theme/index.css": "data:text/plain;base64,LyogcGxhY2Vob2xkZXIgKi8K", - ".vitepress/theme/index.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBUaGVtZSBhcyBUaGVtZVR5cGUgfSBmcm9tICd2aXRlcHJlc3MnOwppbXBvcnQgRGVmYXVsdFRoZW1lIGZyb20gJ3ZpdGVwcmVzcy90aGVtZSc7CgppbXBvcnQgeyByZWdpc3RlckNvbXBvbmVudHMgYXMgcmVnaXN0ZXJTeXN0ZW1Db21wb25lbnRzIH0gZnJvbSAnLi4vLi4vLnN5cy9jb21wb25lbnRzL2luZGV4LnRzJzsKCmV4cG9ydCBjb25zdCBUaGVtZTogVGhlbWVUeXBlID0gewogIGV4dGVuZHM6IERlZmF1bHRUaGVtZSwKICBlbmhhbmNlQXBwKGN0eCkgewogICAgcmVnaXN0ZXJTeXN0ZW1Db21wb25lbnRzKGN0eCk7CiAgfSwKfTsKCmV4cG9ydCBkZWZhdWx0IFRoZW1lOwo=", + ".vitepress/theme/index.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBUaGVtZSBhcyBUaGVtZVR5cGUgfSBmcm9tICd2aXRlcHJlc3MnOwppbXBvcnQgRGVmYXVsdFRoZW1lIGZyb20gJ3ZpdGVwcmVzcy90aGVtZSc7CgppbXBvcnQgeyByZWdpc3RlckNvbXBvbmVudHMgYXMgcmVnaXN0ZXJTeXN0ZW1Db21wb25lbnRzIH0gZnJvbSAnLi4vLi4vLnN5cy91aS9tb2QudHMnOwppbXBvcnQgTGF5b3V0IGZyb20gJy4vTGF5b3V0LnZ1ZSc7CgpleHBvcnQgY29uc3QgVGhlbWU6IFRoZW1lVHlwZSA9IHsKICBleHRlbmRzOiBEZWZhdWx0VGhlbWUsCiAgTGF5b3V0LAogIGVuaGFuY2VBcHAoY3R4KSB7CiAgICByZWdpc3RlclN5c3RlbUNvbXBvbmVudHMoY3R4KTsKICB9LAp9OwoKZXhwb3J0IGRlZmF1bHQgVGhlbWU7Cg==", ".vscode/settings.json": "data:application/json;base64,ewogICJkZW5vLmVuYWJsZSI6IHRydWUsCn0K", - "deno.json": "data:application/json;base64,ewogICJ2ZXJzaW9uIjogIjAuMC4wIiwKICAidGFza3MiOiB7CiAgICAiZGV2IjogImRlbm8gICAgICBydW4gLVJXTkUgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgPEVOVFJZPiAtLWNtZD1kZXYiLAogICAgImJ1aWxkIjogImRlbm8gICAgcnVuIC1SV0UgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgPEVOVFJZPiAtLWNtZD1idWlsZCIsCiAgICAic2VydmUiOiAiZGVubyAgICBydW4gLVJORSAtLWFsbG93LXJ1biAtLWFsbG93LWZmaSA8RU5UUlk+IC0tY21kPXNlcnZlIiwKICAgICJjbGVhbiI6ICJkZW5vICAgIHJ1biAtUldFIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9Y2xlYW4iLAogICAgInVwZ3JhZGUiOiAiZGVubyAgcnVuIC1SV05FIC0tYWxsb3ctcnVuIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9dXBncmFkZSIsCiAgICAiYmFja3VwIjogImRlbm8gICBydW4gLVJXRSAtLWFsbG93LXJ1biAtLWFsbG93LWZmaSA8RU5UUlk+IC0tY21kPWJhY2t1cCIsCiAgICAiaGVscCI6ICJkZW5vICAgICBydW4gLVJFIC0tYWxsb3ctZmZpIDxFTlRSWT4gLS1jbWQ9aGVscCIsCiAgICAic3lzIjogImRlbm8gICAgICBydW4gLVJXTkUgPEVOVFJZX1NZUz4iCiAgfSwKICAiY29tcGlsZXJPcHRpb25zIjogewogICAgInN0cmljdCI6IHRydWUsCiAgICAibGliIjogWyJkZW5vLm5zIiwgImVzbmV4dCIsICJkb20iLCAiZG9tLml0ZXJhYmxlIiwgImRvbS5hc3luY2l0ZXJhYmxlIl0sCiAgICAidHlwZXMiOiBbInZpdGUvY2xpZW50Il0sCiAgICAianN4IjogInJlYWN0LWpzeCIKICB9LAogICJ3b3Jrc3BhY2UiOiBbXSwKICAibm9kZU1vZHVsZXNEaXIiOiAiYXV0byIsCiAgImltcG9ydHMiOiB7CiAgICAiQHN5cy9kcml2ZXItZGVubyI6ICI8RFJJVkVSX0RFTk8+IiwKICAgICJAc3lzL2RyaXZlci12aXRlIjogIjxEUklWRVJfVklURT4iLAogICAgIkBzeXMvZHJpdmVyLXZpdGVwcmVzcyI6ICI8RFJJVkVSX1ZJVEVQUkVTUz4iCiAgfQp9Cg==", - "docs/index.md": "data:text/markdown;base64,IyDwn5GLIEhlbGxvCgpHZW5lcmF0ZWQgd2l0aCBbYEBzeXMvZHJpdmVyLXZpdGVwcmVzc0A8RFJJVkVSX1ZFUj5gXShodHRwczovL2pzci5pby9Ac3lzL2RyaXZlci12aXRlcHJlc3NAPERSSVZFUl9WRVI+KQoKCmBgYHlhbWwKZGVidWc6IHRydWUKY29tcG9uZW50OiBzeXMvdG1wL3VpOkZvbwpub3RlczogfAogIFNob3VsZCBpbXBvcnQ6IEBzeXMvdG1wQDAuMC41Ny91aTo8Rm9vPiAoV0lQKQogIFByb3ZlczoKICAtIHJlYWN0IHJlbmRlcmluZwogIC0gaW1wb3J0IGZyb20gc3lzdGVtIG1vZHVsZSAobW9ub3JlcG8pIHZpYSBKU1IuCiAgLSBjb3JyZWN0IGFsaWFzaW5nIG9mIGZ1bGx5LXF1YWxpZmllZCAiaW1wb3J0IiB0ZXh0IChlZzogIm5wbTpyZWFjdDoxOC4wLjAiIOKGkiAicmVhY3QiKQpgYGAKCgpgYGB5YW1sCmRlYnVnOiB0cnVlCmNvbXBvbmVudDogQ29uY2VwdFBsYXllcgp2aWRlbzogdmltZW8vNzI3OTUxNjc3CnRpbWVzdGFtcHM6IAogICcwMDowMzo1OC4yMTUnOiAKICAgIGltYWdlOiBodHRwczovL3dycGNkLm5ldC9jZG4tY2dpL2ltYWdlZGVsaXZlcnkvQlhsdVF4NGlnZTlHdVcwSWE1NkJIdy8yOGY1YjdlZC02N2QxLTQxOWQtOGRiMC1kOTVhZTkwZTgxMDAvcmVjdGNvbnRhaW4zCmBgYAoKCgpgYGB5YW1sCmRlYnVnOiB0cnVlCmNvbXBvbmVudDogVmlkZW8Kc3JjOiB2aW1lby83Mjc5NTE2NzcKYGBgCgo8cD4mbmJzcDs8L3A+CgotLS0KCgojIE1hcmtkb3duIFtTeW50YXhdKGh0dHBzOi8vbWFya2Rvd24taXQuZ2l0aHViLmlvLykKQSBzYW1wbGUgYnJlYWtkb3duIG9mIHRoZSBtYWluIG1hcmtkb3duIHN5bnRheCBvcHRpb25zIGF2YWlsYWJsZSB0byB5b3UuLi4oIPCfkLcgKToKCgojIyBIMSBIZWFkZXIKIyMjIEgyIEhlYWRlcgojIyMjIEgzIEhlYWRlcgojIyMjIyBINCBIZWFkZXIKCi0gKipCb2xkIHRleHQqKiBhbmQgKml0YWxpYyB0ZXh0KiBjYW4gYmUgZW1waGFzaXplZC4KLSBMaXN0czoKICAtIFVub3JkZXJlZCBpdGVtCiAgLSBBbm90aGVyIGl0ZW0KICAgIC0gU3ViLWl0ZW0KICAgIC0gQW5vdGhlciBzdWItaXRlbQogIC0gVGhpcmQgaXRlbQotIE9yZGVyZWQgbGlzdDoKICAxLiBGaXJzdCBpdGVtCiAgMi4gU2Vjb25kIGl0ZW0KICAzLiBUaGlyZCBpdGVtCgo+IEJsb2NrcXVvdGVzIGFyZSB1c2VmdWwgZm9yIGhpZ2hsaWdodGluZyB0ZXh0LgoKSW5saW5lIGBjb2RlYCBhbmQgY29kZSBibG9ja3M6CgpgYGB0cwpjb25zdCBoZWxsbyA9IChzdWJqZWN0OiBzdHJpbmcpID0+IGDwn5GLIEhlbGxvLCAke3N1YmplY3R9IWA7CmNvbnNvbGUubG9nKGhlbGxvKCdXb3JsZCcpKTsKYGBgCgoKQ29sb3Igc3ludGF4IGhpZ2hsaWdodGluZyBhbGwgbWFqb3IgbW9kZXJuIGxhbmd1YWdlczoKCmBgYHB5dGhvbgojIPCfkYsKcHJpbnQoIkhlbGxvLCBXb3JsZCEiKQpgYGAKCgpMaW5rczogIApbSW50ZXJuYWwgbGlua10oI3NlY3Rpb24pIHwgW0V4dGVybmFsIGxpbmtdKGh0dHBzOi8vZXhhbXBsZS5jb20pICAKCkZvb3Rub3RlIHJlZmVyZW5jZVteMV0uCgpUYWJsZXM6CnwgSGVhZGVyIDEgfCBIZWFkZXIgMiB8CnwtLS0tLS0tLS0tfC0tLS0tLS0tLS18CnwgUm93IDEgICAgfCBEYXRhIDEgICB8CnwgUm93IDIgICAgfCBEYXRhIDIgICB8CgpbXjFdOiBUaGlzIGlzIGEgZm9vdG5vdGUuCgpUaGlzIGlzIGEgcGFyYWdyYXBoIHdpdGggYSBbcmVmZXJlbmNlLXN0eWxlIGxpbmtdW3JlZl0gYW5kIGFub3RoZXIgW2lubGluZSBsaW5rXShodHRwczovL2V4YW1wbGUuY29tKS4KCltyZWZdOiBodHRwczovL2V4YW1wbGUuY29tICJPcHRpb25hbCB0aXRsZSIK", + "deno.json": "data:application/json;base64,ewogICJ2ZXJzaW9uIjogIjAuMC4wIiwKICAidGFza3MiOiB7CiAgICAiZGV2IjogImRlbm8gICAgICBydW4gLVJXTkUgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgICA8RU5UUlk+IC0tY21kPWRldiIsCiAgICAiYnVpbGQiOiAiZGVubyAgICBydW4gLVJXRSAtLWFsbG93LXJ1biAtLWFsbG93LWZmaSAgICA8RU5UUlk+IC0tY21kPWJ1aWxkIiwKICAgICJzZXJ2ZSI6ICJkZW5vICAgIHJ1biAtUk5FIC0tYWxsb3ctcnVuIC0tYWxsb3ctZmZpICAgIDxFTlRSWT4gLS1jbWQ9c2VydmUiLAogICAgImNsZWFuIjogImRlbm8gICAgcnVuIC1SV0UgLS1hbGxvdy1mZmkgICAgICAgICAgICAgICAgPEVOVFJZPiAtLWNtZD1jbGVhbiIsCiAgICAidXBncmFkZSI6ICJkZW5vICBydW4gLVJXTkUgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgICA8RU5UUlk+IC0tY21kPXVwZ3JhZGUiLAogICAgImJhY2t1cCI6ICJkZW5vICAgcnVuIC1SV0UgLS1hbGxvdy1ydW4gLS1hbGxvdy1mZmkgICAgPEVOVFJZPiAtLWNtZD1iYWNrdXAiLAogICAgImhlbHAiOiAiZGVubyAgICAgcnVuIC1SRSAtLWFsbG93LWZmaSAgICAgICAgICAgICAgICAgPEVOVFJZPiAtLWNtZD1oZWxwIgogIH0sCiAgImNvbXBpbGVyT3B0aW9ucyI6IHsKICAgICJzdHJpY3QiOiB0cnVlLAogICAgImxpYiI6IFsiZGVuby5ucyIsICJlc25leHQiLCAiZG9tIiwgImRvbS5pdGVyYWJsZSIsICJkb20uYXN5bmNpdGVyYWJsZSJdLAogICAgInR5cGVzIjogWyJ2aXRlL2NsaWVudCJdLAogICAgImpzeCI6ICJyZWFjdC1qc3giCiAgfSwKICAid29ya3NwYWNlIjogW10sCiAgIm5vZGVNb2R1bGVzRGlyIjogImF1dG8iLAogICJpbXBvcnRNYXAiOiAiLi9pbXBvcnRzLmpzb24iCn0K", + "docs/index.md": "data:text/markdown;base64,IyDwn5GLIEhlbGxvCgpHZW5lcmF0ZWQgd2l0aCBbYEBzeXMvZHJpdmVyLXZpdGVwcmVzc0A8RFJJVkVSX1ZFUj5gXShodHRwczovL2pzci5pby9Ac3lzL2RyaXZlci12aXRlcHJlc3NAPERSSVZFUl9WRVI+KQoKCmBgYHlhbWwKZGVidWc6IHRydWUKY29tcG9uZW50OiBzeXMvdG1wL3VpOkZvbwpub3RlczogfAogIFNob3VsZCBpbXBvcnQ6IEBzeXMvdG1wQDAuMC41Ny91aTo8Rm9vPiAoV0lQKQogIFByb3ZlczoKICAtIHJlYWN0IHJlbmRlcmluZwogIC0gaW1wb3J0IGZyb20gc3lzdGVtIG1vZHVsZSAobW9ub3JlcG8pIHZpYSBKU1IuCiAgLSBjb3JyZWN0IGFsaWFzaW5nIG9mIGZ1bGx5LXF1YWxpZmllZCAiaW1wb3J0IiB0ZXh0IChlZzogIm5wbTpyZWFjdDoxOC4wLjAiIOKGkiAicmVhY3QiKQpgYGAKCgpgYGB5YW1sCmRlYnVnOiB0cnVlCmNvbXBvbmVudDogQ29uY2VwdFBsYXllcgp2aWRlbzogdmltZW8vNzI3OTUxNjc3CnRodW1ibmFpbHM6IHRydWUKdGltZXN0YW1wczogCiAgJzAwOjAwOjAwLjAwMCc6IAogICAgaW1hZ2U6IC9pbWFnZXMvdm9sY2Fuby5qcGcKICAnMDA6MDM6NTguMjE1JzogCiAgICBpbWFnZTogaHR0cHM6Ly93cnBjZC5uZXQvY2RuLWNnaS9pbWFnZWRlbGl2ZXJ5L0JYbHVReDRpZ2U5R3VXMElhNTZCSHcvMjhmNWI3ZWQtNjdkMS00MTlkLThkYjAtZDk1YWU5MGU4MTAwL3JlY3Rjb250YWluMwpgYGAKCiFbaW1hZ2VdKC9pbWFnZXMvdm9sY2Fuby5qcGcpCgoKYGBgeWFtbApkZWJ1ZzogdHJ1ZQpjb21wb25lbnQ6IFZpZGVvCnNyYzogdmltZW8vNzI3OTUxNjc3CmBgYAoKPHA+Jm5ic3A7PC9wPgoKLS0tCgoKIyBNYXJrZG93biBbU3ludGF4XShodHRwczovL21hcmtkb3duLWl0LmdpdGh1Yi5pby8pCkEgc2FtcGxlIGJyZWFrZG93biBvZiB0aGUgbWFpbiBtYXJrZG93biBzeW50YXggb3B0aW9ucyBhdmFpbGFibGUgdG8geW91Li4uKCDwn5C3ICk6CgoKIyMgSDEgSGVhZGVyCiMjIyBIMiBIZWFkZXIKIyMjIyBIMyBIZWFkZXIKIyMjIyMgSDQgSGVhZGVyCgotICoqQm9sZCB0ZXh0KiogYW5kICppdGFsaWMgdGV4dCogY2FuIGJlIGVtcGhhc2l6ZWQuCi0gTGlzdHM6CiAgLSBVbm9yZGVyZWQgaXRlbQogIC0gQW5vdGhlciBpdGVtCiAgICAtIFN1Yi1pdGVtCiAgICAtIEFub3RoZXIgc3ViLWl0ZW0KICAtIFRoaXJkIGl0ZW0KLSBPcmRlcmVkIGxpc3Q6CiAgMS4gRmlyc3QgaXRlbQogIDIuIFNlY29uZCBpdGVtCiAgMy4gVGhpcmQgaXRlbQoKPiBCbG9ja3F1b3RlcyBhcmUgdXNlZnVsIGZvciBoaWdobGlnaHRpbmcgdGV4dC4KCklubGluZSBgY29kZWAgYW5kIGNvZGUgYmxvY2tzOgoKYGBgdHMKY29uc3QgaGVsbG8gPSAoc3ViamVjdDogc3RyaW5nKSA9PiBg8J+RiyBIZWxsbywgJHtzdWJqZWN0fSFgOwpjb25zb2xlLmxvZyhoZWxsbygnV29ybGQnKSk7CmBgYAoKCkNvbG9yIHN5bnRheCBoaWdobGlnaHRpbmcgYWxsIG1ham9yIG1vZGVybiBsYW5ndWFnZXM6CgpgYGBweXRob24KIyDwn5GLCnByaW50KCJIZWxsbywgV29ybGQhIikKYGBgCgoKTGlua3M6ICAKW0ludGVybmFsIGxpbmtdKCNzZWN0aW9uKSB8IFtFeHRlcm5hbCBsaW5rXShodHRwczovL2V4YW1wbGUuY29tKSAgCgpGb290bm90ZSByZWZlcmVuY2VbXjFdLgoKVGFibGVzOgp8IEhlYWRlciAxIHwgSGVhZGVyIDIgfAp8LS0tLS0tLS0tLXwtLS0tLS0tLS0tfAp8IFJvdyAxICAgIHwgRGF0YSAxICAgfAp8IFJvdyAyICAgIHwgRGF0YSAyICAgfAoKW14xXTogVGhpcyBpcyBhIGZvb3Rub3RlLgoKVGhpcyBpcyBhIHBhcmFncmFwaCB3aXRoIGEgW3JlZmVyZW5jZS1zdHlsZSBsaW5rXVtyZWZdIGFuZCBhbm90aGVyIFtpbmxpbmUgbGlua10oaHR0cHM6Ly9leGFtcGxlLmNvbSkuCgpbcmVmXTogaHR0cHM6Ly9leGFtcGxlLmNvbSAiT3B0aW9uYWwgdGl0bGUiCg==", + "docs/public/images/volcano.jpg": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAG9AwwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9Dm06yQlTaWgHXaLaAqD6jMY5/AcV/Kn1jz/F/wCZ6Nn3f9fNEL6bZE7hbWoI5AFtGD26EDaD9F/XmuSpVqSm3FXT63/z1v5htb8/89xosrZeltEf+2Ua/wDsnNZ+2qR3je+vR/roF/n8/wDKw06dZn/l0tj/ANu0J59TlD/Lv1o9tUloo+e3RdNylrokl+P6EL6bahsi0tBnjBtbfoRj/nlxn17VaxU1Fxe0lZq2q9Plun5A4NJtNaJ2W2u66Weq8iNtKtmBH2W1B9rW2/n5Wf0P4VHtI/yy+5f5mHPV7fgiP+yrQDm0tPr9kg/P/VY/THpxxWv1yqkkltotfLTQpUU9Wnrv73f52/DyKcujQHG23s9vHH2SHr+CKOehypH1qlXc/eklfbXyNYpwXKr21et5bvvr91/TsQnSIeQLO0J/687bg/8AfkUe35fK/bcq8ujvby/4L/L8SsdHQZzZ2mMnkQWuevp5XH07CmsSm+rfpf8A9KVvvFzS80/RW+7QjbSolGTaW5+lva//ABmrVf8Arlh6dw5pvrb1j/wSBtOgH/LrCf8AfSJvywowaLvp+QlOS6fr+Yz+zICwZbO1z1/1EIOcc/NtzWiqWsmlpprp/wAAXO30+6z/AAFOlwuPns7YnsTDA2B9SAcZzxg889zUTqq+3T7Nmt35/gXGTad1rftbt0/W/wCQxtKtgBm3gPPH7lcZ/wC+F7Zx1p0ZxlJ25lZX1S2ul3fVoptdLrve36flsxg0y2BBNtbnjBzAjd++5Sp9eVIzxXQ2ktvP+v8Ah0LTq/y1/ra+9gbT7MKT9jtGP91rWHaeP9lFORz6+3Wo9pHs/uX+ZXXT89f8hj2tuoH+hWo6cLDHkcdOVAx2+tR7X0+TX/DmGq/4O3rZIjWK0I5s4seojj498bCOPrz3NV7RNXafySf6r+rnNU1m/l+SJFjs+htoCOgzBGT+q8f0pxlfa/R7DhPlTTvZ22t0v39f60JfstsVO21gU8dIY1bHsypnnvzzWyxHRJOS6adN3rbS/wCJcYNT53brrfVKXR6fm3uIbG3ZebaHPvDG3HplhnHrwfbnmj2839nV+np3Lb0fo+/n5fqiu2l2r43WtuQOmLeLrx6o39KccRVpbprZ6JJdemmnrbVM5b9kvx/zIZNGtSMCGJemP3cZAGegXYF9sdu3QCq9spe81K8tXZLffuO//BS2v3KzaRBHytnasPaCLPfJPyjHv29KPa36S/BfqJv+un9aEP8AZcJ/5c7Xp/DDEf8A0Ein7Ref3IG+1/vEOmxL0tYAR0GxSP8Avg5Xp9R3HNJVE2lrv1S7Prf+th6Wtf7/AL7t6dSvJYqCM21tnHe2h9T/ALFaX/q3/BBadfudv8yudPjzxDbpx1+ywkfTAUdeuc4rd1V2X3k/WJraK+dv8/kINPRTuMVu2Dkj7PEAfbHlnHX17U1Vjp7v3K/6ozc+Ztuyb1av1EezjYjFtar7fZ4j+PMZo+sShpBPXV3/AOAw/rp/mhrWkY4MFsen/LvD/wDEUvrU3o9vLf8AAW39XIjZRH5vKhHqoii28dtnlbSD3B4OTnrR9Y6a9uo47r1X59jOl0+LLlba1YEjrbQDAAI7RjP6kY6jnL9t/dX9M2v/AF/WyKf9nR7vksoFGMFVjjUMQPvE7VI5569OKxlLnb/4FtO39d2YveV31ez/AA01X4/51/7P54swvPZUI69ckk/56VKi0tl3/wCG8hqdu3/gVvwILmwVMH7LGSc5ysfoMYwv6mhwba8tvn/Vh8yldPS3Zuzvruv1MuW1dt2LSHZ/ur2PH8PXpnJ/TFdFOnZKTurbJrWz6v7/AOmJ+zS1fyvf8H/XzK509mHFogz6KmOP+A/5/GtdO/4Ihyo91/5L/WpCdLj4LwBQMHgKCfYYH/1qS+/7tw512Yx9OtxgpDz6OsRHQ+qEn26/jTWslp+HkZvVt9yA2ELctaxNjjmCBj68Zi6c/St15a9f61JIv7OQnLWcH4WtsD9ciA/lRt/X/BAa2nRg5NnblTyQbeAE9uu0Ln/tmBjsetGn+Vv8v+CGpZg0+EtgWFuN2CCbe3IP5RjsffjntQl+G/8AkwPqX9n7whZ3NzealPDGJ1MMUcISH5/ndgREsO3jgBi4we3INeZi8TGgm9fkrO976Nv7/U97K6LqTT9NLbX0srW6PW/fotT7st/BLTWvm/2eFAjVhuijViWXIIIO0EZ67eD1Dd/EnnEXJwuu26bu/Pr1t5vY+qWV1Je/FXjrovJfp18jM/4RK1u3e1ls1QIu538m3lzkMNsitDtx1Jxntwa5auaRirX1ld9Xf8u9t7tXN44Gtro7d9V0tbz/AK+XlusfC+yk1YYtrZYX3ZdbK0KoSMqQPJGADnHUgH6ivOnjOfbVv+m2/wA9r27lfUav2k7er6bf8D5eYWWmeHNAsr60utN067vIzttmit7ZAPWWRliyJCMgAAjuSOM+jh/YzV5u0rXiltt39e/W3mYzo+yt36rqvzulov07VTHoS6e86pYFmRw9ubWMTwSZJMbF4tsmRggocdyTnA670/5vxen9f1czvp/Wn9LZ/wCTPJNTWNpGMMUKINwCpDEuQM45AwBjGMDnGMAGuaVaak1G9r2TSdrXVvw37PYhzS6eXl528+u33aW4y51JImZFAyF53IvXn29O+Kh1KktbPt/L/wAOhOp2t82k+uy/rr8sO4vBdjaQhIycbUHGMDqvPXn269TVQUpys79+77W8v036HPiHzRinbdtW16LtfcwLpYS2wiMgEKw2oc+ufl5HOTzxxXfRp2d+kdu2t9Eu7/zR57lyybey9etkun9fkyTRoby0NwkcAjVtu4wI+TnnqN2B36jHPHSur+rW/re/lfYiUVUW/mmv+HXT017PbidV8OW0qLE8NqWxwn2dC2eRkEA4yCfTr07VdKfs3o+2tuy6pdN/n96PYq97fjv/AF3JdP8Ah9JbRLP5FqoU78JBGCw5PO5QMD6Edj79PtObVLdb22to/TX5k+x1uk9fR/jfrp07diHUNOitmCvY2wyuSxggOeep2p7enU9aynNJ7Pbsu/m9PQVSU42cezu/6/4BkSw2qof9EtWyOn2eE+vrGeePr9KUZKTtbz1S7rt6kyxc1GWmyTtvt5W83/w1y74etyNRt4raxtWknl8ootnGf9ZgKPkUAZDd1ABPOc5qpOyb7JvXXW3Vevnr+euBxU51oRf2pxi7O97ytb0t1f39T3DUPAl5a2/2i4itY1niVooo4494XZlgy7flYbuhPfjPOPFq1bVe/p/S/Tf7/t8PT1vdKz9emr7pNL77WPNL2A2NvcW8UcckhcnmNAwGTkZHQDOO/HIz31p6p+bb8tUnp/X/AANakqiVrNLo9dUvytq737nml/mYSGSGFSDhi8aMQD90jCtnB5AzjGRnHX06drL1dvlbTU8rFU3V956uy33srv1/Hp30fHX80FuRm3gcjILrFGpbHPOAO3Qf412U7Xfon/X9aHium6bemj/T/h9/kc5c3kU7fIgX5TldiYXI6D5T09fTpWsdZL57ejMqycqcktG7L5df67/jgZEhOEXPTPlp+nynnv0rbYzhTsvJa/8AD9yEWau5/cjoTkpGOnb7vJ9sdOuK5Oep5/8AgX6XL+Z2WjeBtS12DfaQMsSk7nSANwRkH0OcYG4DOcYzit/awjFc8mnb3uvpbX5dLm9PD1aq92N4y2f57X6anoWg/DCDT1km1cxoDtCx+TGHZQAd7EDdGdxYbWz0Bxg5PHVxyV1B3Wu2ut2rX2+756HoYfKXKLlO97uyW1vO/wDXpZmrP4W0yKNltsB4ySsnylm3dpPkPXK5I3HOOxrjVZyk3JaNaaWW9/R6J2/pHWsLDD6x3+F+SX5/j1OamJs40RIYQ2GBYxISPmPO/bk+oP8ATp1RV/e6Kz/C9+356mn2X8/z0/z9Dh9RiS4lkLRrlhn7qcP2zjHA/H2reltL1/Q5qmrX9b2/rYyD5VrFLClrbksjDdNCs5BPXG48c5I6gdq7EcLo3bbT1bb95db7a/8AB6HH3quInYxREDfn5EyBk4xweAOgDDgZoj8UfVfmDjyxcfJ+e6uc82H4McW09fkGc+3+evOK6jAXyEQEoqL1JUIhDfX5Qf5Z70AQwyRecyOFAwOqDHQ+mOvY4zzzz1AH3CLwAEIOcDaBwcdcKPwxxnFAE1mpCtmFCMYz5anPA5yc57duM5+mbnFNqz0b7Dp/a9JfmdDFJm1jRIYwyZQsI0DE9TgbcbecDODnPAxyvaR7P8P8yE9/X9EWgsUUGfKVpCpyccd8dfXA7euKpTUtk/n/AF6im5W91X79NO9znLzMrFfLXBPKhAenPGee3Bxyc/U7Qdkn27/5nFOU5Pleid1fv69tSkIlBH7gHAxkxLyfz6c/561ftV5f+Bf8Az9k+8fx/wAjejYOF/dxdP7g9Af8/hWXOuzNrloW5wW2oRjoqjI9uVBx3IBzzxSc10t83boZ8nNNu2ja+f8AkR+Wvdec/wBwdPxNHMn/AMDU6UrGnB5DBMQRth048tQcjvzkY4579xSlJJddxnUwQRy7ttui47lEHXkEHGMjr0654NZuaaas9U10Fbz7dX/TX9aj5PJtch4lPQ8LF2Gf7g9R+NFPr8v1B3v/APbNfoc1fXsbSssaDzcfKBEp2L0+b5cElDjjIyO+AasZWQs64liQggDJRRkd+qcn8x161tCfLG2m7e/6ES3+RUmtkBJ8tdoxglF4z9AP646Cm5c1ttL7O5z1fih8yu2nxlgSgK4J+7lTnocdMdwO3rSW6/xL80eZmG1T18+5XeziQqoii5HGYl9efwr1sP8A5f8Atx4M936f5kaWMK5OyM/WNRz17fSnU2XqcE9l6iSW0RBBjjzjkiMfp/nr6VpSqWSWun3/APDfcc1SHMvxX+Z/UD/wRWgWL9lLxooAAPx58WMMLjr4E+GY56c8flivusgkngpu3/MTP5/u6Wx62Vrlw801r7aT3X8lPX5nwrIiORtJHHJP8vrz0r+a+Vdvuv8Aqz6q1v6/r5jFiBIUcnONwzj/ACP8++bqSi3FSSS20Tey67lxoyk1LmaTa93Tb8PMnFtt75z6D0+tZTqzdtdl2X+R0ewUd1zeeq/X7v8AhyN4APuj68g/zP6/hRCpUvva67L+thTpqKuo21t1+f6ETQMeS2CORx0x/n1pttvXV+n/AAxnH4o/4l+ZH5b/AN8/rRZdn9yOrTsvw/yJfILAc5OAT/nij0I5I3vZa+v+dhDasRkjgc4wO3tnP4d6PuHyQ7fmQPbhsY6jrgev4/Wj+uv6GVRJWsrbkX2NT25PfgH8xzSfy/H9TGTsm0R/Ygeoz9S39TVaf1Y0ppN69vPuuw1rCMdh+Kgfyq/bVLfE/wAPx0uKorOSWllpp5f5kR09cH7o/L1+g/8AQqtO6TbbbSv5/jYhbIiNgB0YD6HH8mqlbs/uRMnZ7vbp/wAMxjaeuOzfieP1/wA9z63CSi29tLbLuOMkm763Wl+nnokQ/wBnpjng9+T7+5rX2y2vden/AAwnzXum7J36Wa7bX+Yh05G6ZI75J6/lS9pH+l/wROUnt+n/AACE6YOfx9P6ml+68v6+Zdl2X3P/ADIP7NC9Vx7549f73+cVStb3bW/r9TCpT5ptptbab9O7a8hn2GIHJ25X1PI/Ammm1dLS+9vu/wCATGju2nK3yS/H8eg77LGMNkHsP85HpSvyvn2e11d/8Doa2kls7fK36iGNBkAdvf8AxrohVlbo3ZvZb/zLT+vxE9mu6f6jNnv+lZSqVJbv8Ec3JLsMMQ6559cY/wAf5Ury26eiDkl2IXjb+FgV6de/cdK0i/dbf4r8NAVGo9UrdNXb9SEwtj+H1+Tg/oB+Hv2pKpF3Wia31B0px8/R3/UgMRJ2liGJx059ffsKfNbVNXQ4QfMuaLt1+7+thTZE8k5/L/Cl7af8z+435Idvxf8AkVGs2Jzyfy/Xj9aqFSbbvK6t2X9Lc56lNx2++23rb8yE25U4aM4HVsnFdEZ6K8kc/K73/wAl/X6i/ZwcYjznphsfzNVzx6tf16A0+z/BfoV3tgTwMe/+e/rU80drr+vQLPs/6/D5FWW1xuOTwMnHsM/y7c/rQuTo1e/fr0tr/XmFn2f9feik0KEHO4nr6fX0H9a017Fe/wCf4f5FdoEwMM4zzken8+fes6XNzS5tuZ9NN3ovwZajfpr+vXQjELhfvMRzyfr/AJ7GumbTldKystO3l/kYVF7z0tov6/r7iGSB2wMjkHOce3sMf59KSXr62M3G+6/H/IqyWMjAgOBnHoe+T09cf5HFVzySa5t99OzJ5I2tb8X/AJlWSwlXgEseuRj8ufp+OcUKUu/4f1/WockO35kX2CR+MEd+SB/MVtov+G/4IyF7JlOCD7dD/h/9entrp+P/AAUBVNsUbjr9B+XQ1pGWmt9+yCzeyFhsZJXCiN5SeAozycjHIOcdu1UpLt+DX/A/q5cabk7Wffp6fqaknh6dBumt2TPRdr8tgEBWzgnADdenp2xlVs0k1vr2S+6/46ehp7NRabV0t9dGvPT79B1lo01zcRW9vA0kkjZ+XOQqj5uMnCjuf1x1aqK/xL/hv6+XVdCYx552Ubrt5H2X+zrp9jpPiBX12OexsUaK3kkeGRZp5J/mjEKEYZ2MRCsFIwOevPjZhB1qUuXe0tevp9259RkkJwrxVSN4tqyf8t9tO3l5Wuj9V5vDGnx6Pa3VsUks9QskubaVWBdY/LwocHBDkAgggcjA9a/OcbTxFGrOSqSjFNO2mnXVb6/8Gx+s4SlTqU4csPctpFd93u+++l76niWuWEOmmdotpGTuIA3FSCcN+PTqcE9s0Qqusoc3S6b6Pfqtf8mZ1qUYTtyx69Pw+Xr/AJnh2t3iATMxKIVLBtxGOcjByAAM8gZzXTTvfRNvZd9fv8t/Q458iUuZLl12X3NX7f1pofOXiTUrayv2MUgDOpZudwJbJUEHIyccgDP4816tGm2lJ3dlZPbVWV15a/kfOVpXlJ6Nf0/np5r5nnWr+JmRjukVlj5IXgMD/CxyCcYB5xweo4rthFytu9OvXz9F87de55tSvBXtFL+rb9F/kcGfETyyNK05RWZyIycbQc4UHHQ8KPXOK6qdG9l+Ov3K3fu3dbozdT3VJaXWy6XV/wAPRenUw73WUeYs+G4ABL44ycDqMnOT6/pXbGhTW8Ff0ehw1KknK92Zc2piXcFGTjjaSD0P93A7cdvxqZ01Hl5Ek27OyvfrbvrsZucnZXb7eT6dPPXcoR3M2dpQ5YqAW69Rknk9un9K6bJUp2VpJOztdp+nf17bmc3aLb1W7Xf+vuPTfAkKXtx/Zl2pjSbmFgDjzN2CjZ4xtAbnnnr0A5G6n89/l/wfxHTqQtZR26Xvvqvnd/d97+orD4M6ff6ZBevFAks0RIJj+UjORyCNucZDE4GMdTXFVxFSE0nUfLppp1+Xy2ufR4fLFOF2ry0emrvbb000V11OP1/4fRaemYkkkABRyCGQdeF29sdeOfUk10U8XK6XM0kvkvv31/y0sc1fL6tNyVtFsvubd/nt+i08P8Q+HIRHII4vMLJjC5yCre/1x+eAa7I1lO17O1t/y02flrbucEsLUUZSVko6SV1rfXrrpa2nnc8yHha5uJ47aCKa5upJVSO1t1LySFuwHJLcqPxrrjKlD3pJJdb36X01fzseYqU6tVwhF3d0utnfye29ke5+Avgrq8slvqd8z6UbCczzRXAeB0WEl1DkDflwgwACeQMcgV5uNzSjC8acU7K7au77X6/8P01PpMp4cr80MRUuo05RquL3cYO7T2dtLX9bPqdF4oi+xQtJPey3cw3K6hdkSIPkjX5i0hwqn5mCkgjBORjyo14V25QSi7Wtrv6ba9fNn1bpwp25FbfRa7W72/p+p88+IfKMjvASpK9mOOQTjqen4V105SSd3+C3su/9aGM5R1vazv0312X5W/E8qugvz72OWJGe2c4Ge/BOe2OpxXqUZSbs3pfsuz6/L9Dzq027tadvKyb9L+ezfqcLqEHzSKyk7f4s9eo+hr0qezfd27OyS/z/AOHPHmlKTbV/v6+n9fccu0JVjsXduzx6d+fw9/8ACrvy63tbr6mNSEXB2WvS1+/zIVspQ3C7fbHY/ify601U/vq39eQoKC0mldtvVtN6b6f15I9O8HeDP7YkR2AYLJGxQ8cDDOpPoQPY/iK56+MVJXS5Vvt/X/DGlHATq1kuf93ezWlmt7W095+ulz6O0zRLwmLSdJtJIn2gLFB+7D7UJMjyHKhQFJJc47dSK8HE4qVSXMpe7K7to10/N3PpcNh4U26agrwVvn9+u/lq9b2IdY8J6nAxMsqzSso3pEwkXIBBXdk7iMLuJOQeOlOlNJK3VXb31d0279NLbLZ2NHGqr+/ZN2S5V+n3W69fLzLVEksz5W113EKzc85/h9uQOnNdkWpx7vRr07r8PJHLVcZJaa3u9fX+ttTi9WDJAoAUvuwCPv7O5I6EDpk9cdOK7KUnaKu7Nrm++3Tpb7rmD0Tt/wAN3t+RwF+zJkgEPz04PHr2+nHXGevPUnBbNLa/X8Dzqk5e15b6LTbzXXtrZ/5nNTs2SxyflPfoc1pzy7/kRf8APb5LyMC6cvHIGPJ+UKcD0B44z3Pv3rppcjhGUrc13v3Tdn22t/w5hUm+Zq+n+aMZowuOAc+w4rfmjZvR2632MrijLfLgL/tDBPHsfX/OK55VlGWrdu3y9Ou4FZ7XMgVAWLcjC5JPc8elH1mG3X17/Ier07mnHpJKBnDZPHIwePx9/qOh6Uvav+a/pYr2cu6+81bOwVBtPynBxkeg/HqP8ntDqK+t/uKThFWa12k0t+/UuCDDYSPce5B29fYnp3//AFVDk7+63a39dzOXLf3VZfr95Z+xvIpDLjtwP6Zxjv7+tXTlK71ey7ESdrWIjoqbQ5Pz4yOOOeD/ABeme1bKctm9Pl/kZSs4t9f+GZRl0tk+p5+UZGPwPXjv2FXzR7owLFppvHzA564weACB+PH/ANemBu21lGN4MYbnJ4IBOB7/AEH+NZ1Faztve7BStKyeq/p+um/YmOn2x+YxIn1yMZ/H8M4571nCUrys7e9+X9f5Gvvu176en9XKwhghlGyJSvBIBIzz1ySfTpz1q7yk7bmlny387fh8tzXl1FBEkKJ5ZTq685zyOec4z0/A0nGVnZNOz1+W5P8AW/4enz7XuZU8rTMBuZtwPzEfQdh9fTseSaKbcfid777advX8NxkC2alweCRnkjPHTHPHT2pwrRqScVdWbV721XVLUzqNppp8sevbzv8A1/kSyW6ABSNxwCTzwD/UfrXTyr183v8AocVfEJVLRaSsr21u9bu92RNbBl2D7vcEEfT3/wA471rTjHW6XT9exzTxL0u02r22X/D/AIEZtMDAOOMAew9O/wDj3rWMYXV1onf/AC6nn16/Pfmd79Lb69/UqyWQbBHOBx7YPTn8R9fyr0KUope6ldLzd99V6X1PGruV/dbW68raWXy11/pUWtigycYJ+v8AWobb3d/kv8jFpPdDRaqx3HIXqTj9eueuKI/EvN/hYmfKktFtpv8AN/I/p6/4IyRpH+yx4yVDx/wvXxWe/X/hBvhqO/0r7zhv/cKn/YVU/wDTVE78Bb2Mrf8APyX/AKTA+KTYJ2XPrwn9BX84KS7r73/mfYxopx5uZ3stNOoqWKhuEzknj5R+OQPQetck/iltvuXTh7sZcz62XRWb/Pf5/MmNkvG5B/47/XJqUk+q/H/gG6ut3cjNhF/dP5g/zq1o+n4/oiJrmjZ3WogsI/7q/XHzD34wMjtVPe/+f/DnNyyU0uVtcyu/K++1mRtZIDgDPrnH/wBbii/9f1c6Pu+SX+TI2s+OOv4Uf1/Wgaf0v8v6/SP7IwPf8v8AA0af1/wwf1vb9CI22Ccx5+g/wo/rcyq/Z+f6DDbZBwjc+xx1yexo1fb8DBq6a2v1363IzakHpj6jn6/d/wA+tFv6uioy5X0elu39bEbWxBzjdn17Y784/Sj+v6sEnzNt9f8AKww2xP8AB191qlOyS009f8iUrKwn2ZR94YP4Hj60/aeSE436tfcRvagD5SrE8YB7YP6UObku2u/Uapc7tz8ttdI79LO179/lci+yLxkqD74zU8z/AJn97NXSjGErNtqOj87/AH+WwfZUHp+AH/16OaX8zfzZhyP+Z/cmQtbAN6/kP06f5+ldapppe89V2v8AcTzNdCtLaM3K/UjjsPr6f/qp83J7q1S9euv6s3glKKbdm3tppa/f7zOlsG5JJ+b26f45/D8auEua90lb11vf1CUVG1nf7ntttf8AEUWCGJD8xPcY/wDr5GP6mlUUpJqPxJq3ou/f+kZX5ny7ef8Aw6t/WgosQo+7nvkj/wDX0qYqrH7Ebb7vT0DkT6tefbzsmhzWyL2/l/QUvaeS/EmSttr6/wDAY02ykEKuTjgcfX+VHtPJfexJO2q6dP8AO/6L5EP2RSMMmDknt7dj+n1rSM04tNR+Lz7f8HqS4yT0lbT+VMabNQBtUfpWcKajOUlO/MrW7K9/+GBJ63le/eK0+5ETWiZyVwR9Ov5ZrX+uv+Q7N2Wn4r9UNNvGOv8An9KPkVyS7L73/wDJEb2K/dUHjk4OD6du3P8AL0FbqKT/AK/T/MTlzbpbfevNX/rYrNp+SQd3Prjv9anm97lsui+1fYxdKLu1Jryt+pWNgUbjI6nGOvb/AD2Bpyla3XTz/wAvz1MZrlaitbq93pb7iN7QY6DOeuF989ATU87/AKuUqcmrr/h+79P89NmVnth8wxngjgjByPTj+VONR80dviW9+/8AXUPZySba0Wr1XT5kJtkUHA5xzkDj6Z9PQ9f0ro9s/wCVfJkf12+8pizGclcg9ML+R71rfYjmd7Naev8AwwNaLjATHUgEAckfQelNOV9lbvf9Nf66EzaSvfXRW8vW5CbI/wB1fzrRKOrckv17/wDA+fYXJKaTg031Umlppt3/AC/SI2WM5Hc8duvrnGP51N9bKzXf+kL2VRb8tv7ru/uTb9dCJrM5+7/I/rVfd+P6WM5XW2v4XXzGCzHcL+CrWntH/Tf+QWGNYo3VRj1wOPw6VPtmnay9b6lct11+9W/K5JDoEs/KQhhnAO1c/wCH07560p1EmrTsrXfvWV/nv6/caQptprW9nJLa9u+3bo+x7d4D+Cs/iPTbzVjJ5CWUgSKG2jaWWeQgNh2UkRoUwQGAJYg5A4rmq4tQimpJ30s29v8APa7XR27nrZZgZ16cqslNSu48q2S3vd27Jflc6ib4baraRtDJYFoXfMasrTO2V8v5QeQerAL74zXPHGRlKK93Vra/4X3s/wCtDsqYCcE/dbt0ez20fZf5H0f+zp+zfa6q1/4s1yzitdO0+VrcfbLRpRPM0SSGOKAnLkiTDNgqFHqTWtbG4ejG8p279PL7/wBfI9HK8klX9/laV9dFq3vq+q62v0ta59Y6r8NPCtw1rMkWmCDTZoGnWJIo9QgRMlWVgqs6qm792u9hkDORivJ/tmGq5ISTb3cu9tl009Hr5n2NPJKcJRkudNKOihG10kunmrafI3fG3i/w5b2UNnosypELdbaKEq+Y40iVHJOAUDPuZjxjOcY5r5bM6nt605wS5W4Winonyq+/9ban0OE56MOTluop6u6aW+n3/d3Z8xa14rtjLPHczxFDkjnaqDGGDHaSc9iTgY9xjCFPSFlqu3X1TfTyv/nx4nEpz9+0e1no1q72+f8AVrng3i7WrZy2y4UW+G3EEgYU/KQeCc9sHkV10YT5rOLW+rW11bZba/138jFYmm6clF63VrddWu3/AAOlz5p8QalBqs8qW8uPLb5dxO5yo6qepwTyPTkZzXu0aba1Vkl0vq/61093ozwJS5JPVSumnFvRdttG1r6fn4b4h1CaGeWF5HQA4DN91s9ce4wOPfiuqjSqO3urV289r2v269N9TjlGMt15fnpd7pdul76HGWerNLefZnkO0rncScFsnHpxkZB5OfSuyGjSas9tOltNDkrLlVlqk9PTbW2yT09e5cv4p451ILuJDhOTggA9wepJyQTwtbPW1lbppdq99/u+9rsckW3fmVmnb5JaW/r8S3YWN9dOnlxyglgCETPXpycdPp17dqG0rtv7/LXa/VdvyKSvfyPSdC8I3OoT28LkpNLLGhDRg7d0gXJzjBAJP4AdCawlXjFvZ2e7ul5/1/kaUqSraO6Xpv632+f/AA/1b4d+B97bXGnajJKfssUil0EDK0jnIjYuhKLvOQV+9xnpxXJVx/JtGL3Su36dl063t+Z6dDK+Zac3oorV7fJdvR9ND7Dsfhxq9pp9uJInihNrG0W9xvKSfMDtBx0U/n74rxcXiHKXMrLyu/6sfX4LCyprlu3dKN7LZbbbW03+XRvgfF3hu1sLckuouGXblsFfmJ4K9+xz1yTjpU0a7aTdrPpd2Tu0l2j+qfa1uvF4S1JNrWS1SXr9+3Tfbq7fKOueDdQ1K5kjsVUnzJN2eA/O7Pynt+vX1r1KOIhGLvo77Xav/XTyun0Pm8TgZzg4U72nrdJb6rz2VrfcdD4B8Cx6LdDVtWSL7Y4U2/lllktnRs5JHRn2j6gHrUYnHuVPkildNX1d7f5dvPyOvKMqhh37SpHnly2tOK67NPV/r3R1niTxFc3UdxZ/akTeXQyAYfphC5HLBRxyc4HFedCSu+aKm56WfS+if5frc+nlCEqfuz9klH4I2ab1Vmnq3v5M+ZPFt5KM2zOHBJ3zZx94EnI+6B2APHU1204KOkVZdlfptb7ne3oeZXSjb3ttNraX36X1X33v0PENUuESd45AGUqCsikkHPODnj/6/rXfGnJR0W9m720utV8r/wBanjVZNzdu/faz1sr9bd/Prc5O7sYJo2bbnJwD+fHAB4zxnrj0rsp1HFpuKvfv303+e/Tqc09Yp+f6O5yd1puC6jOCOOBxz9e/Qj/9VehTrPblVt3vbXTT7v8ANnnTjaVujf4NK/8AX3FKHQAxDDljkgbcnBBz0PoeR+vpcqjad0ku+o5QhCm5uTbW6atq35avTW62avbobFp4dWRiHAXapbc6gAhcccsRz36cn8ahVF/d32T/AK9Px9eOfJK7UmnbpHtfa69dfS6Ow8M302hXeY4hJFIQrpkLx0+Xr3OfqfrlYrDOumrcvpfbTpvt1X5WtOHxzw+0ufvzNJ/h/S8j61+Fctrfw6nfTWxVJIfIHmnAQAffRlUEFm6+1fPYyh7BKMZNuL7W+/Xtb7ux9ZktaWKcpTioqWzT5rrpe/5J22szL8V2j2hlWHDqSzqeDJgjoOxGRgd8+1Z0HKavomnb3dem233/AIXZ6daMeWUpOzi7LqpWWna2t/Sx4F4k8pYtrorNvLbyPmGOM98jp7npXr4eDu7tpON+mjutE13u/keLUjFLmjJyu7Wa+e+nTy/4HjWpXKNM2Rw2RGB3IJA/XjtjiumLkpRjy3V9Xrt3XS2r/Uwm0ot+X9f1/wAOclexrKsjAgFTjHYgnP19P61u/wDgnl1Pj5+rvddN0/6ZyF0hAYZx2z+fT1PHTuK6lt9xpyLe++v4GBPD8pG7PB5X35/z0xW0PhXz/M5KytN720s/kZpjOcHd1xz7/wD6smqMG7bWNGHTWlK7Ruz0GBk+/H9PwzWUoWtJtuz1W176XHKXLBu2ulu1v87/AIeZ2mm+HUSITvEFk/2hyR24z35Gev05yla9mt/NnNLENO3Kumt+/wAuxprooc7gMKMZBAx9P58+1X7JfzfgTLEuOl03f5W76Xv8i5/YUT7RtxwPmHG764PAPTj1x0pOhu+eS9OhLxcbJqWvWN1+HV2Fk0SK3TeoGcdDg545Oc/oPzHIqeRRaXPfrd77/l09fKw4YmU9op2vdtu3Ty8/JvoYkiiEZAYNwD8vB/IfpWqioq6bbfpa33mrlUklaMXrrdtO2uokf7zJbP4+4zTbaTfX5/1sVyycXddFe2qXl+H9dZY4VcnoME8HHIGKz9p5Iykmr2300eny+8seWkI4QA5x2xgjP8xmuwi67r+vmSGMAkqQGHIA7+mf/rduKip9n5/kZxTdV21T/r79hRDJKp3HHQDAz71hHVz/AMWp33St6eXb/g9de5WawYtjj6f5/P8ApW1P4vk/zRnLEQtyuyiuqte/Zr9fQk/s8HClVJOBz1Ofwzz/APqrb5EKtTbS5nr/AIX+hMthGgwsY3dyPx9f5fTIrGrC/W1/0/zvr6IzqV1DZ83m9vw38v8AgEsViANx4HPb17duvP8ALGKlYeKSabTert5r+vyOSeMhyyVlqt9fn0+7YRrNGOSgbsSevHtk/p9K6Kd4x5W76uze9n0+Xfr1PKqTvNtPR6rTX8iCe1jUAooXrnv6Y/ziuinre/l+phUm7L5/15mc8BHTr15+v+FatO2j10t23RyzV03d/wCeqK/kMvA578nGMn6VrTStq3a/TXW25iyA2mPvH+R/pWlzPkXdkRtgDkKD1wc/r0x/ntWlNJu93purfl5GNSkuknrr/wAP95/TR/wRuTZ+y54xGAP+L5eKjx/2I/w3/njNff8ADkUsBU63xVR3t/06ondgI8tKavf97J/+SwVvwPkf7GRxk478f5/ka/mDmfl+J9xGDUbWTegn2bA2gEn1GAPy/wDrVL11uv6+80hCUYK/S7+9scYCuBt7c8Z/p/OlzqOjd/QXzS9WNMD8YG3kHO0/4VfMt/8AL/MTV1bTz9PxAwAnn7x6k8ZOMD2Hbmhy0bs9mNRilqtenr677kDQkEALnr6cH07f/XrNTXdr+vIdrDvs6gZK89wPXvT1eutt7mbi7uz69yBoQW4Vh7Ec/wAjVqVlZ3GoyXX8WRtbnj5SOvUfT/Zp869fn/kw5X5fP/gojMBHVT+Xvj+7T5l/Tf8AmQ2lfTZ20S72I2hbtkf8Bz+uKOZd/wCr+f8AmNR5lslvvb+vuGtbDowAx/e/yDWV/wCk/wDhyLEJt1B6fiOh+laxd1pfQpK/b7iNrdMj73TsCarbtqr9TOcHJq1trdurImt1YY2kc57n+dK/oOEHF3dtrd+q7ob9jXGcH8Vo5tbdfn6lS1TXVrfT/IYbRew/8dx/Knf1+9/8Ex9m+8fv/wCAMMBII2gYGc444HTp/L8K1jO+3N21+Wvb9TJa/fYg+zkA/KwxnnBIH/1hV/1vr+OopKUW126W0/r5EbQggAlcj+8Mfl1znv8AStITUU731/Tz+Y0+6IJIcc46AAEDg/p07jpVqqk7r9f6/wCCTs23trovNlYwH/63P9RVe38vwf8AmVe60TIngLYwDxn+ntUKpC234MUFb4vLbUQW+D909+n0/Csm7t9m337/AHG96fb8BxthgttJx6gk/hz/AJ7VPvaWtbzbREnFvTa3RWGm3AHC9f8AZNXTk03zPS3S/wDkZy6EP2PceQck+lbKpFNPe3k77WJSbkrNXvpdu346CfY1OflJ/Dr9Of8APeuiMlLa3z/PuOTlF2bjf5CfZPlG8Y91Gcn07cY96v8ArUUZwnsn5XVv627foMNkhzy4/wCA/wBc5qLe9fS111dzS3p9xG1gCOCx/wB7t9OCaU05Wt0W2vf7jGpG8ou17X2SX+RQfT2ByB04xg/4Y7VDi0tdfR+pacErWbXoiB7JxkkDH4Z6ZPBGf88Uo7r1XV/1+ANwtta97afIhazI/h/yPwNaOoo3vfS/9aWM9Oi/IhNkQT8oPsOTz37fT+lHtF3lt3/r8TCdNx1bjq3pfXutu4i6bLM22KGSWQkBYo4zJI55I2xj5m6YwDknjvWir2XKrXjr7z6Pp/X/AAyVJVVCEYudRtrlSvour7L/AC2O4vPhdq9npMOqybCZolmNrCrCaKMjJEqyMDEwGQwwcEdTzjkeYfvHF3vfptZ6P8rq/wCZ9BS4WxVTDxqy5IRd2lzPm2Ts42un2111tbr51PaeU7xuduxmGGbJIVsDnAyeOv1Pau2niISs9dfLfTf9X3totTiq5NOg2rtvZNJtebevbUptAjdA3PTj6+/PT8q6FJOzT39TycRRlSaUl62WnT+vxGC1GRwevpUN23/N/wCZjb+maMOlS3BjWKFn3EY2rzwc/wAh9c+5qXN8spLS3d977b/8MbUqbcop+9zbKNnLV7W73/yPp/4VfDD7ffWcV5ptxdC5VWnV4HTyIWAY7FKZd9o3LtHO7qOM+Li8Zd2U9lbe2qe19vS/z12+oy3LHOvF1KT5XDlu077vpa3k97W76n3VoXw80DwnYFNCtzELv5p5HX5ZDkHeY22kj5QpJA9M44rwK2Nnf4nZvRrV/wBJdb+SR+gYDJ6NCi4qKbcr2srctnfTvqXdZ8DXGqWMMtjaxRRqWV7iGCPYsoJPyOWyrE/NnI+bOCABjOOKqfEpr3feV76q/wDwbbb6Gv8AZMakrciV/wCbTv8Af+HT1NvwJc3HhbwvfaRqDSrdNfTzwrLysiMyDMeOAdq4K/N82Buxg0VqssRHlcmtN+1/+Bs9LI6qOClhW401BNPv26aLpq/TY43X/GD2E810jRfOnlhCmCgIO4L13FiQTwOQfTnhftFonJ9n93fW/wDmzrqVJUrOXLqle0r2b1f2bW+d/vPmvxd48gtXfz2IluZPkbJz5jsShA6DHHBIXpuxzjWNCpVUWou19b31tpa+1raefqeZWzCMXL3mtNk9dreW717aaOx86eLvGMkE9wwk3rJEu3cxODj5sbSRzkcHn8QK9ijh0owTXwp8zel+1tN097v703b5+tWnVk5Slor8vXTe3rt+HRHhmq+P72dJYZFBhBOMSEHC5HTHGD0GenUV6MKEHFcvxN726dXbf8Xpqn0POqztdqTVvJPvrq9OnT5nFQ+MtPg8ySc4IDE7iu4tg/KDuHXpxyeBg9/To0OSLstVFv522v3f9a2ZwTrLom7uyvslfV28100/Q8g17xFFr+reZAZI4Y1ZBsYle5YtxkBV7YLNzhTg1VLnjbmila3V+V9Lf137DqxSe7V+ln0v0/W3XyPdPhj8BdV8Z20evPdPY2TGVo5J4i5cIxBWMKOx78gJh/YzKXLLmk073dlq7Wdu3l5/mRZzlpF++73s9nt1X3dWrH2N4J/Y41LxBbprMrzR6dDGXhkvo1SK8kQsknkK5B+UBWXHDAjBJBA4cRjHB3g7R5Vddb312/zv2tY9fCZHVxMfd5L3teTa0sn/AFv6nT6x8BdE8L2T2EccskxlQm6EKZknJPyIVGduOfmKgHtXOsZKqvjTXz7/APAvvt5MdXKfq8nCcU5O69xX133utLPt56Npne+BP2bbxPs3iS4msYoXYiC3u1LSnCj98sak72YZChguCQflIyeWtiNHrd67Prb7r6fqrnZg8mlpZQV9U5eW9738lfXXr2+w9P0LSbPwvYaZaR28c1orSXxlt0d5nEjBUIJJQbcEYPOeOteVOdStK0ZRvf7Tsk/8vy26H0uEwSpaThd94JWv317bd2zzDX/G91YziydsRwIYACdxC5/oQAOmOQOtRJSXutxb9fz1f9eW3pUY0qb96LS7Nfnvor/Pr5+B+NfEcF2DtlDOMnYccnHAHPp9OfStIxnF2aSatd62s9f6d/w3MZOjOnaN2+9rbO+iv8um8jzAa7ZW8L3M5KOpwgQrlj68EcA9Tu69BXZGlOW72S62evS6VvwfU8ulKnzK8brySt+fXTR+Why9/wCLVclxKkUY+6ofJbPQn0bPvxz+PQqKt7usut9trXt+NttdNyqlWCqcsYytbXTz3/E8n8QeLjJM8MciNtZWdwTufhcKOxJ+71weMjsKjSlzR0S97XXzMpVVZu6SS1Ter7pef6+p5P4g177TlVYbyGByMbTwBnPAyen6c8V3wgop39NOiWvlr99tHtc4K1Tnvy320v30v36Xs/6fnk8L3Um98A4AHPbA44UenH616EOSSSStot76/j955s07t6+frd3f9fcQvbOGEasASoIBOAT35I5zzkDPtg81M0oStdJK19Xba/8AX9I53pG2l73t166mt/wg+vXFvDcnTrhY7kB4WKEeehbbmIMoLDPfgHIGTzhfWKS15lpbTr91/wCuvnjLDVpv3Y3U3pvo/u0v37a9TqJPAd9o9jaX01nJFLJxIkqYf5lCjA5/ib29c9KzljaWqTlfS19Uvx662/Eznl+LTvKMVC19JdNbdO/TQuaT4E1LxRf22n2sJimlBw7gqkajPzSMvTqT90k9MjjE/XIq77a6J9L+emw6WVV6rajy9tZW9UtH/SudzqHwS/stI1g1OO6vF5nl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APJdE3bTrbYeJzChgaMW5wVTaScldXve6d7fcfK/g79qX4X/AB38N3fjT4TeO9D8d6LFfzabfXujXLm50rUY40lfTtZ0u7hg1PSrsxSxzwJfW0C3ULeba/aIwZF7sbkmKyuUXi6FbDp2t7anKCk3souStfut+x5azmjjYuWHqU6sbJN0pRkr2tvFu3a69PMx9Y8XJPFtLnziG+YswG48qSRwMDHfrximsM3LmdOS0V04+XTa173T2a1Jlib00uZSkr6JrTXay262v/nfz688SMQscu4JubGW4I6hs4GBnnj9c1pyKO1m+tunlZf56nPSxLpyfMrKUm02rO1nd6/m9f14zVfEWGATcIzw3ORk5IySCeoJ/StqNO89dFa935dm9P6+ZjiMc4z5lFtWautd7aK3otreWl2cNcX800sjhmG88YY9R0IHQHPOe/410+xitbrTzjfv0s/u+RwSrzqSTu0rp226/J39btfnAMOS0uWbA5Y//Xp+zXb8zT2kv5pfJv8AzFEAbG3gEjpnofx6f5zSc1DbW11ZPVfdqTKStv6nsXwn8G6TrepXE2sx+fDZKHhgZSElZjkEsWUYU9QMkjhutcdfEO1n7rtreXW22uv9XNcDho18Vd2a5Uvn+Pl/SPpXUrTS5oY8+Xusx+6RQERVByECoQqIGBAA4DZFeWo1Yyvztqet+a9u999bWf5WZ9E6dKnNRdNPlavbpf8AH1fTfueR+I9cDzBJreKRUL5GVIUKf7vIBwABx8uc9q2hTl7snK7tvzJPVdluvXV9yKkoTmoxpO1nrbtfTRWWun/BM/w/r62V281pabPNIEkilyYk746gfTHyjngkVr7NtW5396+757b67bHPSnGnJxceVPrZruurf4O342g8R+LRJIJLeRtqmQMnmAHOc7mzkkkjHPJBHbiphhrfa6dX0va2/rvt1NKkoxWsktXv6fo327/LzLWfEM14pLSbBg/db5nB4ILADA9s474zxXfSgkknJaLy69F/XfXU5qs1yXUk1ZttbPtpr8u9tdjzTUtTlhVhFgtJwwckBl5H3uuOMZznjFetQhHle2+ye2i7dbb9up4mIrSclZNq20VtqrX89/0Z53qV3uizuXLsAUJBYHDfMQfmAGCPTn3zXRCmruyd7d3tpp+WunTpocMua93Fq/l9y+SOVu2UKCmN3UgHPT27cemP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", "docs/section-a/item-a.md": "data:text/markdown;base64,IyBUaXRsZS1BICAKCioqTG9yZW0gaXBzdW0qKiBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBRdWlzcXVlIG5lYyBxdWFtIGxvcmVtLiBQcmFlc2VudCBmZXJtZW50dW0sIGF1Z3VlIHV0IHBvcnRhIHZhcml1cywgZXJvcyBuaXNsIGV1aXNtb2QgYW50ZSwgYWMgc3VzY2lwaXQgZWxpdCBsaWJlcm8gbmVjIGRvbG9yLiBNb3JiaSBtYWduYSBlbmltLCBtb2xlc3RpZSBub24gYXJjdSBpZCwgdmFyaXVzIHNvbGxpY2l0dWRpbiBuZXF1ZS4gSW4gc2VkIHF1YW0gbWF1cmlzLiBBZW5lYW4gbWkgbmlzbCwgZWxlbWVudHVtIG5vbiBhcmN1IHF1aXMsIHVsdHJpY2VzIHRpbmNpZHVudCBhdWd1ZS4gVml2YW11cyBmZXJtZW50dW0gaWFjdWxpcyB0ZWxsdXMgZmluaWJ1cyBwb3J0dGl0b3IuIE51bGxhIGV1IHB1cnVzIGlkIGRvbG9yIGF1Y3RvciBzdXNjaXBpdC4gSW50ZWdlciBsYWNpbmlhIHNhcGllbiBhdCBhbnRlIHRlbXB1cyB2b2x1dHBhdC4KICA=", "docs/section-a/item-b.md": "data:text/markdown;base64,IyBUaXRsZS1CICAKCioqTG9yZW0gaXBzdW0qKiBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBRdWlzcXVlIG5lYyBxdWFtIGxvcmVtLiBQcmFlc2VudCBmZXJtZW50dW0sIGF1Z3VlIHV0IHBvcnRhIHZhcml1cywgZXJvcyBuaXNsIGV1aXNtb2QgYW50ZSwgYWMgc3VzY2lwaXQgZWxpdCBsaWJlcm8gbmVjIGRvbG9yLiBNb3JiaSBtYWduYSBlbmltLCBtb2xlc3RpZSBub24gYXJjdSBpZCwgdmFyaXVzIHNvbGxpY2l0dWRpbiBuZXF1ZS4gSW4gc2VkIHF1YW0gbWF1cmlzLiBBZW5lYW4gbWkgbmlzbCwgZWxlbWVudHVtIG5vbiBhcmN1IHF1aXMsIHVsdHJpY2VzIHRpbmNpZHVudCBhdWd1ZS4gVml2YW11cyBmZXJtZW50dW0gaWFjdWxpcyB0ZWxsdXMgZmluaWJ1cyBwb3J0dGl0b3IuIE51bGxhIGV1IHB1cnVzIGlkIGRvbG9yIGF1Y3RvciBzdXNjaXBpdC4gSW50ZWdlciBsYWNpbmlhIHNhcGllbiBhdCBhbnRlIHRlbXB1cyB2b2x1dHBhdC4KICA=", - "package.json": "data:application/json;base64,ewogICJkZXBlbmRlbmNpZXMiOiB7CiAgICAiQHZpZHN0YWNrL3JlYWN0IjogIiIsCiAgICAiQHN5cy9zdGQiOiAibnBtOkBqc3Ivc3lzX19zdGQiLAogICAgIkBzeXMvdG1wIjogIm5wbTpAanNyL3N5c19fdG1wIiwKICAgICJAc3lzL3VpLWNzcyI6ICJucG06QGpzci9zeXNfX3VpLWNzcyIsCiAgICAicmVhY3QiOiAiIiwKICAgICJyZWFjdC1kb20iOiAiIiwKICAgICJ5YW1sIjogIiIKICB9LAogICJkZXZEZXBlbmRlbmNpZXMiOiB7CiAgICAiQHR5cGVzL3JlYWN0IjogIiIsCiAgICAiQHR5cGVzL3JlYWN0LWRvbSI6ICIiLAogICAgInZpdGVwcmVzcyI6ICIiLAogICAgInZ1ZSI6ICIiCiAgfQp9Cg==", + "imports.json": "data:application/json;base64,ewogICJpbXBvcnRzIjogewogICAgIkBzeXMvZHJpdmVyLWRlbm8iOiAianNyOkBzeXMvZHJpdmVyLWRlbm9AMC4wLjk3IiwKICAgICJAc3lzL2RyaXZlci12aXRlIjogImpzcjpAc3lzL2RyaXZlci12aXRlQDAuMC4xMzciLAogICAgIkBzeXMvZHJpdmVyLXZpdGVwcmVzcyI6ICJqc3I6QHN5cy9kcml2ZXItdml0ZXByZXNzQDAuMC4zMDIiLAogICAgIkBkZW5vL3ZpdGUtcGx1Z2luIjogIm5wbTpAZGVuby92aXRlLXBsdWdpbkAxLjAuNCIsCiAgICAiQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djIjogIm5wbTpAdml0ZWpzL3BsdWdpbi1yZWFjdC1zd2NAMy44LjAiCiAgfQp9Cg==", + "package.json": "data:application/json;base64,ewogICJkZXBlbmRlbmNpZXMiOiB7CiAgICAiQHZpZHN0YWNrL3JlYWN0IjogIm5wbTpAdmlkc3RhY2svcmVhY3RAMS4xMi4xMiIsCiAgICAiQHN5cy9zdGQiOiAibnBtOkBqc3Ivc3lzX19zdGRAMC4wLjE0MSIsCiAgICAiQHN5cy90bXAiOiAibnBtOkBqc3Ivc3lzX190bXBAMC4wLjExMCIsCiAgICAiQHN5cy91aS1jc3MiOiAibnBtOkBqc3Ivc3lzX191aS1jc3NAMC4wLjgxIiwKICAgICJyZWFjdCI6ICJucG06cmVhY3RAMTguMy4xIiwKICAgICJyZWFjdC1kb20iOiAibnBtOnJlYWN0LWRvbUAxOC4zLjEiLAogICAgInlhbWwiOiAibnBtOnlhbWxAMi43LjAiCiAgfSwKICAiZGV2RGVwZW5kZW5jaWVzIjogewogICAgIkB0eXBlcy9yZWFjdCI6ICJucG06QHR5cGVzL3JlYWN0QDE4LjMuMTgiLAogICAgIkB0eXBlcy9yZWFjdC1kb20iOiAibnBtOkB0eXBlcy9yZWFjdC1kb21AMTguMy41IiwKICAgICJ2aXRlIjogIm5wbTp2aXRlQDYuMS4xIiwKICAgICJ2aXRlcHJlc3MiOiAibnBtOnZpdGVwcmVzc0AxLjYuMyIsCiAgICAidnVlIjogIm5wbTp2dWVAMy41LjEzIgogIH0KfQo=", "src/config.ts": "data:application/typescript;base64,CmV4cG9ydCBjb25zdCBDb25maWcgPSB7CiAgdGl0bGU6ICdVbnRpdGxlZCcsCiAgZGVzY3JpcHRpb246ICcnLCAvLyBSZW5kZXJlZCBpbiB0aGUgaGVhZCBvZiBlYWNoIHBhZ2UsIHVzZWZ1bCBmb3IgU0VPLgp9IGFzIGNvbnN0Owo=", "src/nav.ts": "data:application/typescript;base64,aW1wb3J0IHR5cGUgeyBEZWZhdWx0VGhlbWUgfSBmcm9tICd2aXRlcHJlc3MnOwoKLyoqCiAqIExlZnQgc2lkZWJhciBuYXZpZ2F0aW9uLgogKgogKiBEb2N1bWVudGF0aW9uOgogKiBodHRwczovL3ZpdGVwcmVzcy5kZXYvcmVmZXJlbmNlL2RlZmF1bHQtdGhlbWUtc2lkZWJhcgogKi8KZXhwb3J0IGNvbnN0IHNpZGViYXI6IERlZmF1bHRUaGVtZS5TaWRlYmFyID0gWwogIHsKICAgIHRleHQ6ICdTZWN0aW9uIFRpdGxlIEEnLAogICAgaXRlbXM6IFsKICAgICAgeyB0ZXh0OiAnSXRlbS1BJywgbGluazogJ3NlY3Rpb24tYS9pdGVtLWEnIH0sCiAgICAgIHsgdGV4dDogJ0l0ZW0tQicsIGxpbms6ICdzZWN0aW9uLWEvaXRlbS1iJyB9LAogICAgXSwKICB9LApdOwo=" } diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.ts index 46b17591d0..09d195664a 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.ts @@ -1,10 +1,12 @@ -import { type t } from './common.ts'; -import { create } from './m.Tmpl.create.ts'; +import type { t } from './common.ts'; import { Bundle } from './m.Bundle.ts'; -import { update } from './m.Tmpl.update.ts'; +import { create } from './u.create.ts'; +import { prep } from './u.prep.ts'; +import { write } from './u.write.ts'; export const VitepressTmpl: t.VitepressTmplLib = { Bundle, + prep, create, - update, + write, }; diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/t.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/t.ts index 9e738fbd17..f0dbf60cc9 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/t.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/t.ts @@ -11,8 +11,11 @@ export type VitepressTmplLib = { /** Creates an instance of the template file generator. */ create(args: t.VitepressTmplCreateArgs): Promise<t.Tmpl>; - /** Initialize the local machine environment with latest templates */ - update(args?: t.VitepressTmplUpdateArgs): Promise<t.VitepressTmplUpdateResponse>; + /** Write and process the templates to the local file-system. */ + write(args?: t.VitepressTmplWriteArgs): Promise<t.VitepressTmplWriteResponse>; + + /** Prepare the template with latest state and dependency versions. */ + prep(options?: { silent?: boolean }): Promise<t.ViteTmplPrepResponse>; }; /** Arguments passed to the `VitepressTmpl.create` method. */ @@ -34,7 +37,7 @@ export type VitepressBundleLib = { }; /** Arguments passed to the `VitePress.Tmpl.update` method. */ -export type VitepressTmplUpdateArgs = { +export type VitepressTmplWriteArgs = { force?: boolean; inDir?: t.StringDir; srcDir?: t.StringDir; @@ -45,4 +48,4 @@ export type VitepressTmplUpdateArgs = { /** * The response returned from an environment update. */ -export type VitepressTmplUpdateResponse = { readonly ops: t.TmplFileOperation[] }; +export type VitepressTmplWriteResponse = { readonly ops: t.TmplFileOperation[] }; diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.create.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.create.ts similarity index 70% rename from code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.create.ts rename to code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.create.ts index 35e62c8188..b5476effa2 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.create.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.create.ts @@ -1,6 +1,6 @@ import { type t, Fs, PATHS, Tmpl } from './common.ts'; import { Bundle } from './m.Bundle.ts'; -import { createFileProcessor } from './u.file.ts'; +import { createFileProcessor } from './u.process.file.ts'; /** * Create a new instance of the bundled file template. @@ -11,7 +11,7 @@ export const create: t.VitepressTmplLib['create'] = async (args) => { /** * Ensure the templates are hydrated and ready to use. */ - const beforeCopy: t.TmplCopyHandler = async () => { + const beforeWrite: t.TmplWriteHandler = async () => { await Fs.remove(templatesDir); await Bundle.toFilesystem(templatesDir); }; @@ -19,11 +19,11 @@ export const create: t.VitepressTmplLib['create'] = async (args) => { /** * (š·) Perform additional setup here (as needed). */ - const afterCopy: t.TmplCopyHandler = async (e) => {}; + const afterWrite: t.TmplWriteHandler = async (e) => {}; /** * Template-engine instance. */ const processFile = createFileProcessor(args); - return Tmpl.create(templatesDir, { processFile, beforeCopy, afterCopy }); + return Tmpl.create(templatesDir, { processFile, beforeWrite, afterWrite }); }; diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.prep.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.prep.ts new file mode 100644 index 0000000000..9c68126af6 --- /dev/null +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.prep.ts @@ -0,0 +1,55 @@ +import { type t, c, Cli, Esm, Fs, getWorkspaceModules, Semver } from './common.ts'; + +type O = Record<string, string>; + +/** + * Prepare the template with latest state, including making updates to deps/versions. + */ +export const prep: t.VitepressTmplLib['prep'] = async (options = {}) => { + const { modules } = await getWorkspaceModules(); + let deps: O = {}; + + const updateDenoJson = async () => { + const path = './src/-tmpl/imports.json'; + const current = (await Fs.readJson<t.DenoImportMapJson>(path)).data; + const imports = modules.latest(current?.imports ?? {}); + const next = { ...current, imports }; + await Fs.writeJson(path, next); + deps = { ...deps, ...imports }; + }; + + const updatePackageJson = async () => { + const path = './src/-tmpl/package.json'; + const current = (await Fs.readJson<t.PkgJsonNode>(path)).data; + const next: t.PkgJsonNode = { + ...current, + dependencies: modules.latest(current?.dependencies ?? {}), + devDependencies: modules.latest(current?.devDependencies ?? {}), + }; + + await Fs.writeJson(path, next); + deps = { ...deps, ...next.dependencies, ...next.devDependencies }; + }; + + await updateDenoJson(); + await updatePackageJson(); + + if (!options.silent) { + const table = Cli.table([]); + Object.entries(deps).forEach(([key, value]) => { + const m = Esm.parse(value); + const pkg = c.gray(` ${key}`); + const registry = c.gray(m.registry.toUpperCase()); + const version = Semver.Fmt.colorize(m.version); + table.push([pkg, version, registry]); + }); + + console.info(); + console.info(c.italic(c.gray('imports.json'))); + console.info(c.brightGreen(`Dependencies:`)); + console.info(table.toString()); + console.info(); + } + + return { deps }; +}; diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.file.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.process.file.ts similarity index 55% rename from code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.file.ts rename to code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.process.file.ts index 56a91e4d76..5f9b0250c7 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.file.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.process.file.ts @@ -1,7 +1,4 @@ -import { pkg as pkgDeno } from '@sys/driver-deno'; -import { pkg as pkgVite } from '@sys/driver-vite'; -import { Main } from '@sys/main/cmd'; -import { type t, c, DenoDeps, Esm, Fs, PATHS, pkg, Pkg } from './common.ts'; +import { type t, PATHS, pkg } from './common.ts'; /** * File processing rules for the template. @@ -9,18 +6,6 @@ import { type t, c, DenoDeps, Esm, Fs, PATHS, pkg, Pkg } from './common.ts'; export function createFileProcessor(args: t.VitepressTmplCreateArgs): t.TmplProcessFile { const { srcDir = PATHS.srcDir } = args; - const getDeps = async (base: t.StringDir) => { - const from = DenoDeps.from; - const join = (...parts: string[]) => Fs.join(base, ...parts); - const load = async (path: string) => (await from(join(path))).data?.modules.items ?? []; - - const m1 = await load('.sys/deps.yaml'); - const m2 = await load('.sys/deps.sys.yaml'); - const modules = Esm.modules([...m1, ...m2]); - - return { modules }; - }; - return async (e) => { if (e.target.exists && is.userspace(e.target.relative)) { /** @@ -30,40 +15,18 @@ export function createFileProcessor(args: t.VitepressTmplCreateArgs): t.TmplProc return e.exclude('user-space'); } + if (e.contentType !== 'text') return; + if (e.target.relative === 'deno.json') { /** * Update versions in `deno.json`: */ const version = args.version ?? pkg.version; const importUri = `jsr:${pkg.name}@${version}`; - const text = e.text.tmpl - .replace(/<ENTRY>/g, `${importUri}/main`) - .replace(/<ENTRY_MAIN>/, `jsr:${Pkg.toString(Main.pkg)}`) - .replace(/<DRIVER_DENO>/, `jsr:${Pkg.toString(pkgDeno)}`) - .replace(/<DRIVER_VITE>/, `jsr:${Pkg.toString(pkgVite)}`) - .replace(/<DRIVER_VITEPRESS>/, `jsr:${Pkg.toString(pkg)}`); - + const text = e.text.tmpl.replace(/<ENTRY>/g, `${importUri}/main`); return e.modify(text); } - if (e.target.relative === 'package.json') { - const { modules } = await getDeps(e.target.base); - const pkg = (await Fs.readJson<t.PkgJsonNode>(e.tmpl.absolute)).data; - const next = { - ...pkg, - dependencies: modules.latest(pkg?.dependencies ?? {}), - devDependencies: modules.latest(pkg?.devDependencies ?? {}), - }; - - console.info(c.gray(`Resolved versions:`)); - console.info(c.brightCyan(c.bold(`./package.json:`))); - console.info(next); - console.info(); - - const json = `${JSON.stringify(next, null, ' ')}\n`; - return e.modify(json); - } - if (e.target.relative === 'docs/index.md') { const text = e.text.tmpl.replace(/\<DRIVER_VER\>/g, pkg.version); return e.modify(text); diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.update.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.write.ts similarity index 68% rename from code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.update.ts rename to code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.write.ts index 995f8a8fb0..c2783638be 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/m.Tmpl.update.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress.Tmpl/u.write.ts @@ -1,17 +1,17 @@ import { type t, c, Fs, Tmpl } from './common.ts'; -import { create } from './m.Tmpl.create.ts'; +import { create } from './u.create.ts'; /** - * Initialize the local machine environment with latest templates + * Write and process the templates to the local file-system. */ -export const update: t.VitepressTmplLib['update'] = async (args = {}) => { +export const write: t.VitepressTmplLib['write'] = async (args = {}) => { const { inDir = '', srcDir, version, force = false, silent = false } = args; /** * Update template files. */ const tmpl = await create({ inDir, srcDir, version }); - const copied = await tmpl.copy(inDir, { force }); + const copied = await tmpl.write(inDir, { force }); const { ops } = copied; /** @@ -19,7 +19,7 @@ export const update: t.VitepressTmplLib['update'] = async (args = {}) => { * eg. migration change patching. */ const remove = (...path: string[]) => Fs.remove(Fs.join(inDir, ...path)); - // await remove('./path/to/obsolete/file'); + // await remove('./<path>'); /** * Finish up. diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress/-backup.test.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress/-backup.test.ts index d87d9ea96d..62e295e9b3 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress/-backup.test.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress/-backup.test.ts @@ -3,8 +3,8 @@ import { Sample } from '../m.Vitepress/-u.ts'; import { Vitepress } from '../m.Vitepress/mod.ts'; describe('cmd: backup (shapshot)', () => { - const assertExists = async (dir: string, exists = true) => { - expect(await Fs.exists(dir)).to.eql(exists, dir); + const assertExists = async (path: string, exists = true) => { + expect(await Fs.exists(path)).to.eql(exists, `path should exist: ${path}`); }; it('perform backup copy', { sanitizeResources: false, sanitizeOps: false }, async () => { @@ -14,19 +14,20 @@ describe('cmd: backup (shapshot)', () => { * - ā build (dist) * - ā backup (snapshot) */ - await Testing.retry(3, async () => { - const test = async (args: Pick<t.VitepressBackupArgs, 'includeDist'> = {}) => { + const test = async (args: Pick<t.VitepressBackupArgs, 'includeDist'> = {}) => { + await Testing.retry(2, async () => { const { includeDist } = args; const sample = Sample.init({}); + const cwd = sample.path; const inDir = sample.path; const backupDir = Fs.join(inDir, PATHS.backup); const distDir = Fs.join(inDir, PATHS.dist); const silent = true; - await Vitepress.Tmpl.update({ inDir, silent }); + await Vitepress.Tmpl.write({ inDir, silent }); await assertExists(distDir, false); // NB: not yet built. - await Vitepress.build({ inDir, silent }); + const buildResponse = await Vitepress.build({ inDir, silent }); await assertExists(backupDir, false); // NB: not yet backed up. const res = await Vitepress.backup({ dir: inDir, includeDist }); @@ -47,12 +48,12 @@ describe('cmd: backup (shapshot)', () => { await assertTargetExists('docs', true); await assertTargetExists('src', true); await assertTargetExists('deno.json', true); - await assertTargetExists('package.json', true); + await assertTargetExists('imports.json', true); await assertTargetExists('.gitignore', true); - }; + }); + }; - await test({}); // default: excludes the /dist folder. - await test({ includeDist: true }); - }); + await test({}); // default: excludes the /dist folder. + await test({ includeDist: true }); }); }); diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress/-build.test.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress/-build.test.ts index 36e9729cfb..ceb3f837e2 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress/-build.test.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress/-build.test.ts @@ -24,7 +24,7 @@ describe('Vitepress.build', () => { const inDir = Fs.resolve(sample.path); const outDir = Fs.resolve(sample.path, 'dist'); - await Vitepress.Tmpl.update({ inDir }); + await Vitepress.Tmpl.write({ inDir }); const res = await Vitepress.build({ pkg, inDir, silent: false }); expect(res.ok).to.eql(true); @@ -65,7 +65,7 @@ describe('Vitepress.build', () => { const outDir = Fs.resolve(sample.path, '.vitepress/dist'); expect(await Fs.exists(outDir)).to.eql(false); // NB: clean initial condition. - await Vitepress.Tmpl.update({ inDir }); + await Vitepress.Tmpl.write({ inDir }); const res = await Vitepress.build({ pkg, inDir, outDir, silent: true }); expect(res.ok).to.eql(true); diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress/-dev.test.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress/-dev.test.ts index 8a60a74f57..188608cd48 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress/-dev.test.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress/-dev.test.ts @@ -11,7 +11,7 @@ describe('VitePress.dev', () => { const open = false; const sample = Sample.init(); const { port, inDir } = sample; - await Vitepress.Tmpl.update({ inDir }); + await Vitepress.Tmpl.write({ inDir }); const server = await Vitepress.dev({ port, inDir, open }); // NB: await returns after Vitepress as completed it's startup. try { @@ -43,7 +43,7 @@ describe('VitePress.dev', () => { const open = false; const sample = Sample.init(); const { port, inDir } = sample; - await Vitepress.Tmpl.update({ inDir }); + await Vitepress.Tmpl.write({ inDir }); const server = await Vitepress.dev({ port, inDir, open }); await server.dispose(); diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress/-u.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress/-u.ts index 5404691efb..2219d3f1d4 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress/-u.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress/-u.ts @@ -43,8 +43,8 @@ export const assertEnvExists = async (dir: t.StringDir, expected = true) => { await assert('.gitignore'); await assert('.vitepress/config.ts'); await assert('.vitepress/theme/index.ts'); - await assert('.sys/-main.ts'); await assert('deno.json'); + await assert('imports.json'); await assert('package.json'); await assert('docs/index.md'); }; diff --git a/code/sys.driver/driver-vitepress/src/m.Vitepress/u.build.ts b/code/sys.driver/driver-vitepress/src/m.Vitepress/u.build.ts index 81dd199ea7..85a2bf420e 100644 --- a/code/sys.driver/driver-vitepress/src/m.Vitepress/u.build.ts +++ b/code/sys.driver/driver-vitepress/src/m.Vitepress/u.build.ts @@ -11,29 +11,46 @@ export const build: B = async (input = {}) => { const options = wrangle.options(input); const { pkg, srcDir = 'docs', silent = false } = options; - const spinner = Cli.spinner(c.gray('building...'), { start: false }); - if (!silent) spinner.start(); - const dirs = wrangle.dirs(options); const inDir = dirs.in; const outDir = dirs.out; + if (!silent) { + const table = Cli.table([]); + const push = (label: string, ...value: string[]) => table.push([c.gray(label), ...value]); + push('Directory:', c.gray(`${Fs.cwd()}/`)); + push(' in:', Fs.trimCwd(dirs.in)); + push(' out:', Fs.trimCwd(dirs.out)); + + console.info(c.bold(c.brightGreen('Paths'))); + console.info(table.toString().trim()); + console.info(); + } + let params = `--outDir=${outDir}`; if (srcDir) params += ` --srcDir=${srcDir}`; const cmd = `deno run -A --node-modules-dir npm:vitepress build ${inDir} ${params}`; const args = cmd.split(' ').slice(1); + + const spinner = Cli.spinner(c.gray('building...'), { start: false }); + if (!silent) spinner.start(); + const output = await Process.invoke({ args, silent: true }); const ok = output.success; spinner?.clear().stop(); - // Write {pkg} into /dist so it's included within the digest-hash. + /** + * Write {pkg} into /dist so it's included within the digest-hash. + */ if (pkg) { const path = Fs.join(dirs.out, 'assets', '-pkg.json'); await Fs.writeJson(path, pkg); } - // Calculate the `/dist.json` file and digest-hash. + /** + * Calculate the digest-hash and store it in the [dist.json] file. + */ const entry = './index.html'; const dist = (await Pkg.Dist.compute({ dir: dirs.out, pkg, entry, save: true })).dist; const elapsed = timer.elapsed.msec; diff --git a/code/sys.driver/driver-vitepress/src/pkg.ts b/code/sys.driver/driver-vitepress/src/pkg.ts index 79cb3f9c80..fc97160599 100644 --- a/code/sys.driver/driver-vitepress/src/pkg.ts +++ b/code/sys.driver/driver-vitepress/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/driver-vitepress', version: '0.0.302' }; diff --git a/code/sys.driver/driver-vitepress/src/types.ts b/code/sys.driver/driver-vitepress/src/types.ts index 41c005ea17..aafcbde0b6 100644 --- a/code/sys.driver/driver-vitepress/src/types.ts +++ b/code/sys.driver/driver-vitepress/src/types.ts @@ -7,5 +7,3 @@ export type * from './-entry/t.ts'; export type * from './m.Vitepress.Log/t.ts'; export type * from './m.Vitepress.Tmpl/t.ts'; export type * from './m.Vitepress/t.ts'; - -export type * from './ui/components/t.ts'; diff --git a/code/sys.driver/driver-vitepress/src/ui/-.test.ts b/code/sys.driver/driver-vitepress/src/ui/-.test.ts deleted file mode 100644 index 6107677d00..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/-.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type t, describe, it, expect } from '../-test.ts'; - -describe('UI.Components', () => { - it('import ā API', async () => { - const { FOO } = await import('@sys/driver-vitepress/ui'); - console.log('FOO', FOO); - }); -}); diff --git a/code/sys.driver/driver-vitepress/src/ui/common/mod.ts b/code/sys.driver/driver-vitepress/src/ui/common/mod.ts deleted file mode 100644 index 049ed939e5..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/common/mod.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type * as t from './t.ts'; - -/** - * Libs - */ -export { rx } from '@sys/std'; -export { Color, css } from '@sys/ui-css'; -export { FC } from '@sys/ui-react'; diff --git a/code/sys.driver/driver-vitepress/src/ui/common/t.ts b/code/sys.driver/driver-vitepress/src/ui/common/t.ts deleted file mode 100644 index 95ae0dbb3a..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/common/t.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { CssValue } from '@sys/ui-css/t'; - -export type * from '../../common/t.ts'; diff --git a/code/sys.driver/driver-vitepress/src/ui/components/t.Time.ts b/code/sys.driver/driver-vitepress/src/ui/components/t.Time.ts deleted file mode 100644 index 0e181545ba..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/components/t.Time.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { t } from './common.ts'; - -/** - * https://w3c.github.io/webvtt - * https://en.wikipedia.org/wiki/WebVTT - * - * @example - * - * WEBVTT - * - * 1 - * 00:00:00.165 --> 00:00:01.735 - * This is a model for understanding - * - * 2 - * 00:00:01.875 --> 00:00:03.335 - * and working with group scale. - * - */ -export type Timestamps = { [key: StringTime]: Timestamp }; -export type Timestamp = { - image?: t.StringUrl; -}; - -export type StringTime = string; // HH:MM:SS.mmm diff --git a/code/sys.driver/driver-vitepress/src/ui/components/t.ts b/code/sys.driver/driver-vitepress/src/ui/components/t.ts deleted file mode 100644 index 32cc611faa..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/components/t.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { t } from './common.ts'; -export type * from './t.Time.ts'; - -export type YamlFences = ConceptPlayerDef; - -export type YamlFenceBase = { - debug?: boolean; -}; - -/** - * A YAML block that defines a concept player. - */ -export type ConceptPlayerDef = YamlFenceBase & { - component: 'ConceptPlayer'; - video?: StringVideoUri; - timestamps?: t.Timestamps; -}; - -export type StringVideoUri = StringVimeoUri; -export type StringVimeoUri = t.StringUri; // "vimeo/<id>" diff --git a/code/sys.driver/driver-vitepress/src/ui/components/u.ConceptPlayer.tsx b/code/sys.driver/driver-vitepress/src/ui/components/u.ConceptPlayer.tsx deleted file mode 100644 index 3cf6eb0862..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/components/u.ConceptPlayer.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { type t, Color, css, FC, rx } from './common.ts'; - -export type ConceptPlayerProps = { - theme?: t.CommonTheme; - style?: t.CssInput; -}; - -export const ConceptPlayer: React.FC<ConceptPlayerProps> = (props) => { - const {} = props; - - /** - * Render - */ - const t = (ms: t.Msecs, ...attr: string[]) => attr.map((prop) => `${prop} ${ms}ms ease-in-out`); - const transition = t(50, 'opacity'); - const theme = Color.theme(props.theme); - const styles = { - base: css({ - backgroundColor: 'rgba(255, 0, 0, 0.1)' /* RED */, - color: theme.fg, - }), - }; - - return ( - <div style={css(styles.base, props.style)}> - <div>{`š· ConceptPlayer`}</div> - </div> - ); -}; diff --git a/code/sys.driver/driver-vitepress/src/ui/mod.ts b/code/sys.driver/driver-vitepress/src/ui/mod.ts deleted file mode 100644 index 5471b6a4a3..0000000000 --- a/code/sys.driver/driver-vitepress/src/ui/mod.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @module - * UI Components - */ -export const FOO = 'š·'; diff --git a/code/sys.tmp/scripts/-clean.ts b/code/sys.tmp/-scripts/-clean.ts similarity index 100% rename from code/sys.tmp/scripts/-clean.ts rename to code/sys.tmp/-scripts/-clean.ts diff --git a/code/sys.tmp/-scripts/-tmp.ts b/code/sys.tmp/-scripts/-tmp.ts new file mode 100644 index 0000000000..625639163e --- /dev/null +++ b/code/sys.tmp/-scripts/-tmp.ts @@ -0,0 +1 @@ +console.log('tmp', 'š·'); diff --git a/code/sys.tmp/-tmp/-sample.lib/-.test.tsx b/code/sys.tmp/-tmp/-sample.lib/-.test.tsx deleted file mode 100644 index 408f6eb4c8..0000000000 --- a/code/sys.tmp/-tmp/-sample.lib/-.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// import { type t, describe, it, expect } from '../-test.ts'; -// -// describe('TMP', () => { -// it('load', async () => { -// // const m1 = await import('./my.js'); -// const m2 = await import('./dist/entry.lib.mjs'); -// -// console.log(`-------------------------------------------`); -// console.log('m2', m2); -// -// const foo = await m2.fn(); -// console.log(`-------------------------------------------`); -// console.log('foo', foo); -// -// const Component = foo.FooSample; -// -// const el = <Component />; -// -// console.log('el', el); -// -// // console.log('foo.msg', foo.msg); -// }); -// }); diff --git a/code/sys.tmp/.gitignore b/code/sys.tmp/.gitignore new file mode 100644 index 0000000000..409ee189ef --- /dev/null +++ b/code/sys.tmp/.gitignore @@ -0,0 +1 @@ +npm/ diff --git a/code/sys.tmp/deno.json b/code/sys.tmp/deno.json index 45a7b2374a..adb9e31252 100644 --- a/code/sys.tmp/deno.json +++ b/code/sys.tmp/deno.json @@ -1,18 +1,18 @@ { "name": "@sys/tmp", - "version": "0.0.95", + "version": "0.0.110", "license": "MIT", "tasks": { "test": "deno test -RW", "lint": "deno lint", "dry": "deno publish --allow-dirty --dry-run", - "clean": "deno run -RWE ./scripts/-clean.ts" + "clean": "deno run -RWE ./-scripts/-clean.ts", + "tmp": "deno run -A ./-scripts/-tmp.ts" }, "exports": { ".": "./src/mod.ts", "./t": "./src/types.ts", "./types": "./src/types.ts", - "./tmp": "./src/mod.tmp.ts", "./ui": "./src/ui/mod.ts", "./sample-imports": "./src/-sample/-sample-imports.ts" } diff --git a/code/sys.tmp/src/common/libs.ts b/code/sys.tmp/src/common/libs.ts index 2fe37cdfca..c9c4908f65 100644 --- a/code/sys.tmp/src/common/libs.ts +++ b/code/sys.tmp/src/common/libs.ts @@ -1 +1 @@ -export { rx, Time, Pkg } from '@sys/std'; +export { isRecord, Path, Pkg, rx, Time } from '@sys/std'; diff --git a/code/sys.tmp/src/mod.tmp.ts b/code/sys.tmp/src/mod.tmp.ts deleted file mode 100644 index 0bd335e846..0000000000 --- a/code/sys.tmp/src/mod.tmp.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @module - * Temp module export - * - * ```ts - * import { Tmp } from '@sys/tmp/tmp'; - * ``` - */ - -export type TmpLib = { count: number }; -export const Tmp: TmpLib = { count: 0 }; diff --git a/code/sys.tmp/src/pkg.ts b/code/sys.tmp/src/pkg.ts index 79cb3f9c80..838ac10ec9 100644 --- a/code/sys.tmp/src/pkg.ts +++ b/code/sys.tmp/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/tmp', version: '0.0.110' }; diff --git a/code/sys.tmp/src/types.ts b/code/sys.tmp/src/types.ts index 0f2f882807..b1a0424bf3 100644 --- a/code/sys.tmp/src/types.ts +++ b/code/sys.tmp/src/types.ts @@ -2,4 +2,4 @@ * @module * Module types. */ -export {}; +export type * from './ui/Foo/t.ts'; diff --git a/code/sys.tmp/src/ui/Foo/api.Signals.ts b/code/sys.tmp/src/ui/Foo/api.Signals.ts new file mode 100644 index 0000000000..e31e2f4f74 --- /dev/null +++ b/code/sys.tmp/src/ui/Foo/api.Signals.ts @@ -0,0 +1,2 @@ +import { Signal } from '@sys/ui-react'; +export const count = Signal.create(0); diff --git a/code/sys.tmp/src/ui/Foo/common.ts b/code/sys.tmp/src/ui/Foo/common.ts new file mode 100644 index 0000000000..30ca005008 --- /dev/null +++ b/code/sys.tmp/src/ui/Foo/common.ts @@ -0,0 +1,2 @@ +export * from '../common.ts'; +export { Color } from '@sys/color'; diff --git a/code/sys.tmp/src/ui/Foo/mod.ts b/code/sys.tmp/src/ui/Foo/mod.ts new file mode 100644 index 0000000000..ee815bfe34 --- /dev/null +++ b/code/sys.tmp/src/ui/Foo/mod.ts @@ -0,0 +1 @@ +export * from './ui.tsx'; diff --git a/code/sys.tmp/src/ui/Foo/t.ts b/code/sys.tmp/src/ui/Foo/t.ts new file mode 100644 index 0000000000..0486df5f9e --- /dev/null +++ b/code/sys.tmp/src/ui/Foo/t.ts @@ -0,0 +1,6 @@ +import type { t } from './common.ts'; + +export type FooProps = { + enabled?: boolean; + style?: t.CssInput; +}; diff --git a/code/sys.tmp/src/ui/Foo/ui.tsx b/code/sys.tmp/src/ui/Foo/ui.tsx new file mode 100644 index 0000000000..8ef05e6f6a --- /dev/null +++ b/code/sys.tmp/src/ui/Foo/ui.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { count } from './api.Signals.ts'; +import { type t, css, pkg, Signal } from './common.ts'; + +/** + * Component (UI). + */ +export const Foo: React.FC<t.FooProps> = (props) => { + const { enabled = true } = props; + let text = `import ā ${pkg.name}@${pkg.version}/ui:<Foo> | ā”ļø:signal/count:${count.value}`; + + const [isOver, setOver] = useState(false); + const over = (isOver: boolean) => () => setOver(isOver); + + const [, setRender] = useState(0); + const redraw = () => setRender((n) => n + 1); + + /** + * Lifecycle + */ + Signal.useSignalEffect(() => { + count.value; + redraw(); + }); + + /** + * Render: + */ + const styles = { + base: css({ + display: 'inline-block', + cursor: 'pointer', + backgroundColor: `rgba(255, 0, 0, ${isOver ? 0.1 : 0.03})` /* RED */, + }), + code: css({}), + }; + + if (!enabled) text += ' (disabled)'; + + return ( + <div + className={styles.base.class} + onMouseEnter={over(true)} + onMouseLeave={over(false)} + onClick={() => count.value++} + > + <code className={styles.code.class}>š· {text}</code> + </div> + ); +}; diff --git a/code/sys.tmp/src/ui/common.ts b/code/sys.tmp/src/ui/common.ts index a34108591b..1ae8dab12c 100644 --- a/code/sys.tmp/src/ui/common.ts +++ b/code/sys.tmp/src/ui/common.ts @@ -1,2 +1,16 @@ +/** + * @external + */ +import { signal, useSignal, useSignalEffect, effect } from '@preact/signals-react'; +export const Signal = { + signal, + effect, + useSignal, + useSignalEffect, +} as const; + +/** + * @system + */ export { Color, css, Style } from '@sys/ui-css'; export * from '../common.ts'; diff --git a/code/sys.tmp/src/ui/mod.ts b/code/sys.tmp/src/ui/mod.ts index 533b790110..c165919e89 100644 --- a/code/sys.tmp/src/ui/mod.ts +++ b/code/sys.tmp/src/ui/mod.ts @@ -2,5 +2,4 @@ * @module * Sample UI modules and components. */ -export { Foo } from './ui.Foo.tsx'; -export { VideoPlayer } from './ui.VideoPlayer.tsx'; +export { Foo } from './Foo/mod.ts'; diff --git a/code/sys.tmp/src/ui/ui.Foo.tsx b/code/sys.tmp/src/ui/ui.Foo.tsx deleted file mode 100644 index 3e796001e3..0000000000 --- a/code/sys.tmp/src/ui/ui.Foo.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { type t, css, pkg } from './common.ts'; - -/** - * Sample properties. - */ -export type FooProps = { - enabled?: boolean; - style?: t.CssInput; -}; - -/** - * Component (UI). - */ -export const Foo: React.FC<FooProps> = (props) => { - const { enabled = true } = props; - let text = `${pkg.name}@${pkg.version}/ui:<Foo>`; - - const [isOver, setOver] = React.useState(false); - const over = (isOver: boolean) => () => setOver(isOver); - - const styles = { - base: css({ - display: 'inline-block', - backgroundColor: `rgba(255, 0, 0, ${isOver ? 0.9 : 0.3})` /* RED */, - }), - }; - - if (!enabled) text += ' (disabled)'; - return ( - <div className={styles.base.class} onMouseEnter={over(true)} onMouseLeave={over(false)}> - <code>š· {text}</code> - </div> - ); -}; diff --git a/code/sys.tmp/src/ui/ui.VideoPlayer.tsx b/code/sys.tmp/src/ui/ui.VideoPlayer.tsx deleted file mode 100644 index 61f264a277..0000000000 --- a/code/sys.tmp/src/ui/ui.VideoPlayer.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-types="@types/react" -import React from 'react'; -import { type t, css } from './common.ts'; - -import '@vidstack/react/player/styles/base.css'; -import '@vidstack/react/player/styles/plyr/theme.css'; - -import { MediaPlayer, MediaProvider } from '@vidstack/react'; -import { PlyrLayout, plyrLayoutIcons } from '@vidstack/react/player/layouts/plyr'; - -export const DEFAULTS = { - src: 'vimeo/499921561', // Tubes. -} as const; - -/** - * Sample properties. - */ -export type VideoPlayerProps = { - title?: string; - src?: string; - style?: t.CssInput; -}; - -/** - * Component (UI). - */ -export const VideoPlayer: React.FC<VideoPlayerProps> = (props) => { - const src = props.src || DEFAULTS.src; - - const styles = { - base: css({}), - }; - - const elPlayer = ( - <MediaPlayer title={props.title} src={src} playsInline={true}> - <MediaProvider /> - <PlyrLayout - // thumbnails="https://files.vidstack.io/sprite-fight/thumbnails.vtt" - icons={plyrLayoutIcons} - /> - </MediaPlayer> - ); - - return <div className={css(styles.base, props.style).class}>{elPlayer}</div>; -}; diff --git a/code/sys.ui/ui-css/README.md b/code/sys.ui/ui-css/README.md index 0448a7a3d3..905e0824dd 100644 --- a/code/sys.ui/ui-css/README.md +++ b/code/sys.ui/ui-css/README.md @@ -1,5 +1,5 @@ # Style/CSS -Tools for working with Styles/CSS programatically (aka "css-in-js"). +Tools for working with strongly-typed styles/css programatically (aka. "css-in-js"). Note: This approach is a pure JS-to-CSS/DOM approach, with zero-dependencies on any other special bundler plugin, or post-css processing type dependencies, which inevitably cause @@ -12,15 +12,28 @@ Applying within a JSX programming style idiom: ```tsx import { css } from '@sys/ui-css'; -export function Component(props:{}) { - const styles = { - base: css({ padding: 10 }), - }; - return <div className={style.base.class}>{'š Hello World!'}</div> +export function MyComponent() { + const base = css({ padding: 10 }) + return <div className={base.class}>{'š Hello World!'}</div> } ``` +If your component takes styles as an incoming property, these can be merged with +internal component styles like so: + + +```tsx +import { css, type CssInput } from '@sys/ui-css'; + +export function MyComponent(props: { style?: CssInput } = {}) { + const base = css({ padding: 10 }) + return <div className={css(base, props.style).class}>{'š Hello'}</div> +} +``` + + + ### CSSProps Transformation pipeline. @@ -58,3 +71,55 @@ The CSS class-name is generated from the hash of the style ( `\<prefix\>-<hx>` ) and calls to this function are memoized, keyed on the hash, to ensure the function is safe to use in "render heavy" frameworks like React (et al.). +## Scope + +### @container + +The @container contextual rule-block, used for creating media-queries at the component level, can be +applied like so: + +```tsx +const styles = { + base: css({ + containerType: 'size', // š¼ ā NB: enable @container rules (width AND height). + // containerType: 'inline-size', // š¼ ā NB: enable @container rules (width only). + }), + + h2: css({ + color: 'red', + fontSize: 50, + transition: 'font-size 200ms, color 200ms', + }) + .container('min-width: 400px', { fontSize: 90, color: 'blue' }) + .container('min-width: 600px', { fontSize: 150, color: 'salmon' }) + .container('max-height: 470px', { display: 'none' }).done, + +}; + +return ( + <div className={styles.base.class}> + <h2 className={styles.h2.class}>š Hello</h2> + </div> +); +``` + +Arbitrary CSS selectors can be scoped within a base container like so: + +```tsx +const base = css({ position: 'relative' }) + .rule('h2', { color: 'red' }) + .rule('h2 code', { color: 'blue' }) + + +return ( + <div className={base.class}> + <h2> + {`š Hello`} <code>World</code> + </h2> + </div> +); +``` + + +--- +- [cross-ref:cast](https://warpcast.com/pjc/0x59783042): conversational description, [template expansions](https://warpcast.com/pjc/0xa908939e) \ No newline at end of file diff --git a/code/sys.ui/ui-css/deno.json b/code/sys.ui/ui-css/deno.json index 18155c239b..72c94a0331 100644 --- a/code/sys.ui/ui-css/deno.json +++ b/code/sys.ui/ui-css/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/ui-css", - "version": "0.0.70", + "version": "0.0.81", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys.ui/ui-css/src/-test/common.ts b/code/sys.ui/ui-css/src/-test/common.ts index 9a23332674..659277240d 100644 --- a/code/sys.ui/ui-css/src/-test/common.ts +++ b/code/sys.ui/ui-css/src/-test/common.ts @@ -1,2 +1,2 @@ -export { c, describe, DomMock, expect, it, Testing } from '@sys/testing/server'; +export { c } from '@sys/color/ansi'; export * from '../common.ts'; diff --git a/code/sys.ui/ui-css/src/-test/mod.ts b/code/sys.ui/ui-css/src/-test/mod.ts index ad7ecebfd7..26777a717b 100644 --- a/code/sys.ui/ui-css/src/-test/mod.ts +++ b/code/sys.ui/ui-css/src/-test/mod.ts @@ -1,3 +1,5 @@ +export { beforeEach, describe, DomMock, expect, it, Testing } from '@sys/testing/server'; + export * from './common.ts'; export * from './u.Find.ts'; export * from './u.Print.ts'; diff --git a/code/sys.ui/ui-css/src/-test/u.Find.ts b/code/sys.ui/ui-css/src/-test/u.Find.ts index 8aee355a52..a2ee464b9a 100644 --- a/code/sys.ui/ui-css/src/-test/u.Find.ts +++ b/code/sys.ui/ui-css/src/-test/u.Find.ts @@ -2,21 +2,53 @@ * Helpers for querying the DOM of CSS rules. */ export const FindCss = { - rule(className: string): CSSStyleRule | undefined { + rule(className: string): CSSStyleRule | CSSGroupingRule | undefined { return FindCss.rules(className)[0]; }, - rules(className: string): CSSStyleRule[] { - const res: CSSStyleRule[] = []; + rules(className: string): (CSSStyleRule | CSSGroupingRule)[] { + const res: (CSSStyleRule | CSSGroupingRule)[] = []; const selector = `.${className.replace(/^\./, '')}`.trim(); - for (const sheet of document.styleSheets) { - try { - for (const rule of sheet.cssRules) { - const text = ((rule as any).selectorText || '').trim(); - if (text.startsWith(selector)) res.push(rule as CSSStyleRule); + + // š³ Recursive function to search through CSSRuleList. + // If a grouping rule (e.g. @container) contains a matching rule, + // then return the grouping rule so that the context details are visible. + const searchRules = (ruleList: CSSRuleList): (CSSStyleRule | CSSGroupingRule)[] => { + const found: (CSSStyleRule | CSSGroupingRule)[] = []; + for (const rule of Array.from(ruleList)) { + // Check if this is a grouping rule with nested rules. + if ( + 'cssRules' in rule && + rule.cssRules && + typeof rule.cssText === 'string' && + rule.cssText.trim().startsWith('@') + ) { + // Recursively search its nested rules. + const nestedFound = searchRules(rule.cssRules as any); + if (nestedFound.length > 0) { + // If any nested rule matches, return the grouping rule itself. + found.push(rule as CSSGroupingRule); + continue; + } + } + // Otherwise, if this is a CSSStyleRule, check its selector. + if ('selectorText' in rule && rule.selectorText) { + const text = (rule as any).selectorText.trim(); + if (text.startsWith(selector)) { + found.push(rule as CSSStyleRule); + } } + } + return found; + }; + + for (const sheet of Array.from(document.styleSheets)) { + try { + const found = searchRules(sheet.cssRules); + res.push(...found); } catch (error) { - continue; // NB: Some styleSheets might not be accessible due to cross-origin restrictions. + // NB: Some styleSheets might not be accessible due to cross-origin restrictions. + continue; } } return res; diff --git a/code/sys.ui/ui-css/src/-test/u.Print.ts b/code/sys.ui/ui-css/src/-test/u.Print.ts index a763766862..28f4bdcbef 100644 --- a/code/sys.ui/ui-css/src/-test/u.Print.ts +++ b/code/sys.ui/ui-css/src/-test/u.Print.ts @@ -1,14 +1,34 @@ -import { type t, c } from './common.ts'; +import { type t, c, Str } from './common.ts'; + +const info = console.info; +const cyan = c.brightCyan; +const y = c.yellow; export const TestPrint = { transformed(m: t.CssTransformed) { - console.info(); - console.info(c.brightCyan(`CssTransform:`)); - console.info(m); - console.info(); - console.info(`ā.${c.brightCyan('style')}:`, m.style); - console.info(`ā.${c.brightCyan('class')}:`, `"${c.yellow(m.class)}"`); - console.info(`ā.${c.brightCyan('toString()')}:`, `"${c.yellow(m.toString())}"`); - console.info(); + info(); + info(cyan(`CssTransformed:`)); + info(m); + info(); + info(`ā.${cyan('style')}:`, m.style); + info(`ā.${cyan('class')}:`, `"${y(m.class)}"`); + info(`ā.${cyan('toString()')}:`, `"${y(m.toString())}" ${c.gray('ā default: CssRule')}`); + info( + `ā.${cyan('toString(CssSelector)')}:`, + `"${y(Str.truncate(m.toString('CssSelector'), 60))}"`, + ); + info(); + }, + + container(m: t.CssDomContainerBlock) { + info(); + info(cyan(`CssDomContainerBlock:`)); + info(m); + info(`ā.${cyan('toString()')}:`, `"${y(m.toString())}" ${c.gray('ā default: QueryCondition')}`); + info( + `ā.${cyan('toString(CssSelector)')}:`, + `"${y(Str.truncate(m.toString('CssSelector'), 50))}"`, + ); + info(); }, }; diff --git a/code/sys.ui/ui-css/src/common/t.ts b/code/sys.ui/ui-css/src/common/t.ts index 5119073c6e..9f3baf0288 100644 --- a/code/sys.ui/ui-css/src/common/t.ts +++ b/code/sys.ui/ui-css/src/common/t.ts @@ -1,3 +1,3 @@ export type { ColorLib } from '@sys/color/t'; -export type { Falsy } from '@sys/types'; +export type * from '@sys/types'; export type * from '../types.ts'; diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/-.test.ts b/code/sys.ui/ui-css/src/m.Css.Dom/-.test.ts index f4addb6954..8ae54c6a84 100644 --- a/code/sys.ui/ui-css/src/m.Css.Dom/-.test.ts +++ b/code/sys.ui/ui-css/src/m.Css.Dom/-.test.ts @@ -1,33 +1,83 @@ -import { type t, describe, DomMock, expect, FindCss, it, pkg } from '../-test.ts'; +import { type t, describe, DomMock, expect, FindCss, it, pkg, slug } from '../-test.ts'; import { css } from '../m.Style/mod.ts'; import { DEFAULT } from './common.ts'; import { CssDom } from './mod.ts'; +import { getStylesheetId } from './u.ts'; + +const toString = CssDom.toString; describe( 'Style: CSS ClassName', - - /** NB: leaked timers left around by the "happy-dom" module. */ - { sanitizeOps: false, sanitizeResources: false }, - + { sanitizeOps: false, sanitizeResources: false }, // ā because: "Happy-Dom" () => { DomMock.polyfill(); - describe('create (instance)', () => { - it('prefix: default', () => { - const a = CssDom.create(''); - const b = CssDom.create(' '); - const c = CssDom.create(); - expect(a.prefix).to.eql(DEFAULT.prefix); - expect(b.prefix).to.eql(DEFAULT.prefix); - expect(c.prefix).to.eql(DEFAULT.prefix); + let _count = 0; + const setup = () => { + _count++; + const sheet = CssDom.stylesheet({ instance: `mysheet-${_count}` }); + const classes = sheet.classes(`foo-${_count}`); + return { sheet, classes } as const; + }; + + describe('factory: create <Stylesheet> instance', () => { + it('instance id: default and custom', () => { + const a = CssDom.stylesheet({}); + const b = CssDom.stylesheet({ instance: ' foo ' }); + expect(a.id).to.eql(pkg.name); + expect(a.rules.list).to.eql([]); + expect(b.id).to.eql(`${pkg.name}:foo`); + }); + + it('instance id (as string param)', () => { + const id = slug(); + const sheet = CssDom.stylesheet(id); + expect(sheet.id).to.eql(`${pkg.name}:${id}`); + }); + + it('singleton pooling (instance reuse on data-id)', () => { + const a = CssDom.stylesheet(); + const b = CssDom.stylesheet({ instance: ' ' }); + const c = CssDom.stylesheet({ instance: 'bar' }); + expect(a).to.equal(b); + expect(a).to.not.equal(c); + }); + + it('insert root <style> into DOM (singleton)', () => { + const test = (instance?: t.StringId, classPrefix?: string) => { + const id = getStylesheetId(instance, classPrefix); + + const find = () => document.querySelector(`style[data-controller="${id}"]`); + CssDom.stylesheet({ instance, classPrefix }); + + expect(find()).to.exist; + CssDom.stylesheet({ instance, classPrefix }); + expect(find()).to.equal(find()); // NB: singleton. + }; + + test(); + test('foobar'); + test('foobar', 'my-class-prefix'); }); + }); - it('custom prefix', () => { + describe('.classes(): prefixed class/style DOM insertion', () => { + it('should create <classes> API with default prefix', () => { + const dom = CssDom.stylesheet(); + const a = dom.classes(); + const b = dom.classes(); + expect(a.prefix).to.eql(DEFAULT.classPrefix); + expect(a).to.equal(b); + }); + + it('should create <classes> API with custom prefix', () => { const test = (prefix: string, expected: string) => { - const ns = CssDom.create(prefix); - expect(ns.prefix).to.eql(expected); + const sample = setup(); + const classes = sample.sheet.classes(prefix); + expect(classes.prefix).to.eql(expected); }; test('foo', 'foo'); + test('.foo', 'foo'); test(' foo ', 'foo'); test(' foo- ', 'foo'); // NB: trimmed. test(' foo-- ', 'foo'); @@ -35,25 +85,19 @@ describe( test('foo-123', 'foo-123'); }); - it('pooling (instance reuse keyed on "prefix")', () => { - const a = CssDom.create(); - const b = CssDom.create(DEFAULT.prefix); - const c = CssDom.create('foo'); - expect(a).to.equal(b); - expect(a).to.not.equal(c); - }); - - it('insert root <style> into DOM (singleton)', () => { - const find = () => document.querySelector(`style[data-controller="${pkg.name}"]`); - CssDom.create(); - expect(find()).to.exist; - CssDom.create(); - expect(find()).to.equal(find()); // Singleton. + it('should pass default "classPrefix" value to .classes() API', () => { + const a = CssDom.stylesheet({ instance: slug() }); + const b = CssDom.stylesheet({ instance: slug(), classPrefix: 'foo' }); + const styleA = a.classes().add({ fontSize: 16 }); + const styleB = b.classes().add({ fontSize: 16 }); + expect(styleA.startsWith('sys-')).to.eql(true); + expect(styleB.startsWith('foo-')).to.eql(true); }); it('throw: invalid prefix', () => { + const { sheet } = setup(); const test = (prefix: string) => { - const fn = () => CssDom.create(prefix); + const fn = () => sheet.classes(prefix); expect(fn).to.throw( /String must start with a letter and can contain letters, digits, and hyphens \(hyphen not allowed at the beginning\)/, ); @@ -64,29 +108,21 @@ describe( test('-'); test('foo*bar'); }); - }); - - describe('class/style DOM insertion', () => { - let count = 0; - const setup = (): t.CssDom => { - count++; - const prefix = `sample${count}`; - return CssDom.create(prefix); - }; - it('simple ("hx" not passed)', () => { - const dom = setup(); + it('add: simple ("hx" hash not passed)', () => { + const { sheet } = setup(); + const classes = sheet.classes(); const m = css({ fontSize: 32, display: 'grid', PaddingX: [5, 10] }); - expect(dom.classes.length).to.eql(0); // NB: no "inserted classes" yet. + expect(classes.names.length).to.eql(0); // NB: no "inserted classes" yet. // Baseline: ensure the rule is not yet within the DOM. - const className = `${dom.prefix}-${m.hx}`; + const className = `${classes.prefix}-${m.hx}`; expect(FindCss.rule(className)).to.eql(undefined); // NB: nothing inserted yet. - const a = dom.class(m.style); - const b = dom.class(m.style); + const a = classes.add(m.style); + const b = classes.add(m.style); - expect(dom.classes.length).to.eql(1); // NB: not added twice. + expect(classes.names.length).to.eql(1); // NB: not added twice. expect(a).to.eql(b); expect(a).to.eql(className); @@ -95,28 +131,264 @@ describe( expect(rule?.cssText).to.eql(`.${className} { ${m.toString()} }`); }); - it('hash passed as parameter', () => { - const dom = setup(); - const m = css({ fontSize: 32, display: 'grid' }); + it('add: CSS template ā { Absolute: 0 }', () => { + const { sheet } = setup(); + const classes = sheet.classes(); + const className = classes.add({ Absolute: 0 }); + expect(FindCss.rule(className)?.cssText).to.include( + '{ position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; }', + ); + }); + + it('add: hash passed as parameter', () => { + const { sheet } = setup(); + const classes = sheet.classes(); + const { style, hx } = css({ fontSize: 32, display: 'grid' }); - const className = `${dom.prefix}-${m.hx}`; + const className = `${classes.prefix}-${hx}`; expect(FindCss.rule(className)).to.eql(undefined); // NB: nothing inserted yet. - dom.class(m.style, m.hx); + sheet.classes().add(style, { hx }); const rule = FindCss.rule(className); - expect(rule?.cssText).to.eql(`.${className} { ${m.toString()} }`); + expect(rule?.cssText).to.eql(`.${className} { ${toString(style)} }`); }); - describe('pseudo-class', () => { + it('add: CSS template ā { Absolute: 0 }', () => { + const { sheet } = setup(); + const a = sheet.rules.add('.foo', { Absolute: 0 }); + const b = sheet.rule('.bar', { Absolute: 0 }); + + const expected = { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }; + expect(a[0].style).to.eql(expected); + expect(b[0].style).to.eql(expected); + }); + + describe('pseudo-classes', () => { it(':hover', () => { - const dom = setup(); - const m = css({ color: 'red', ':hover': { color: ' salmon ' } }); - const className = dom.class(m.style, m.hx); + const { sheet } = setup(); + const classes = sheet.classes(); + const { style, hx } = css({ color: 'red', ':hover': { color: ' salmon ' } }); + const className = classes.add(style, { hx }); const rules = FindCss.rules(className); expect(rules[0].cssText).to.eql(`.${className} { color: red; }`); expect(rules[1].cssText).to.eql(`.${className}:hover { color: salmon; }`); }); }); }); + + describe('.rule(): arbitrary CSS-selector DOM insertion', () => { + it('should insert a simple rule into the stylesheet', () => { + const { sheet } = setup(); + const selector = '.test-rule'; + const style = { color: 'blue', margin: 10 }; + + // Pre-condition. + expect(FindCss.rule(selector)).to.eql(undefined); + expect(sheet.rules.list).to.eql([]); + expect(sheet.rules.length).to.eql(0); + + // Insert the rule. + const res = sheet.rule(selector, style); + expect(res.length).to.eql(1); + expect(res[0].selector).to.eql(selector); + expect(res[0].style).to.eql(style); + + expect(sheet.rules.length).to.eql(1); + expect(sheet.rules.list.length).to.eql(1); + expect(sheet.rules.list).to.eql(res); + + // Verify that the rule is inserted in the DOM. + const rule = FindCss.rule(selector); + expect(rule).to.exist; + expect(rule?.cssText).to.eql(`${selector} { ${toString(style)} }`); + expect(rule?.cssText).to.eql(`.test-rule { color: blue; margin: 10px; }`); // NB: ā (same/same). + }); + + it('should not insert the same rule twice', () => { + const { sheet } = setup(); + const selector = '.test-rule'; + const style = { margin: 10 }; + + const insert = () => sheet.rule(selector, style); + const res = insert(); + + expect(res.length).to.eql(1); + expect(res[0].selector).to.eql(selector); + expect(res[0].style).to.eql(style); + + // NB: further calls do not add more items. + expect(insert()).to.eql([]); + expect(insert()).to.eql([]); + expect(sheet.rules.list.length).to.eql(1); + }); + + describe('rules within context-blocks', () => { + it('should insert a simple rule within an "@container" context', () => { + const { sheet } = setup(); + const selector = `.test-container-${slug()}`; + const style = { color: 'blue', margin: 10 }; + const context = '@container (min-width: 700px)'; + expect(FindCss.rule(selector)).to.eql(undefined); + + // Insert the rule within an "@container" context. + const res = sheet.rule(selector, style, { context }); + expect(res.length).to.eql(1); + expect(res[0].selector).to.eql(selector); + expect(res[0].style).to.eql(style); + + // Verify that the rule is inserted in the DOM, wrapped in the context block. + const rule = FindCss.rule(selector); + expect(rule).to.exist; + const expectedCssText = `${context} { ${selector} { ${toString(style)} } }`; + expect(rule?.cssText).to.eql(expectedCssText); + }); + + it('should insert multiple rules within an "@container" context', () => { + const { sheet } = setup(); + const selector = `.test-container-${slug()}`; + const context = '@container (min-width: 700px)'; + const styles = [ + { color: 'blue', margin: 10 }, + { backgroundColor: 'yellow', padding: 5 }, + ]; + + // Pre-check: Ensure no rule exists for the selector. + expect(FindCss.rules(selector)).to.eql([]); + + // Insert the rules with an "@container" context. + const res = sheet.rule(selector, styles, { context }); + expect(res.length).to.eql(2); + expect(res[0].style).to.eql(styles[0]); + expect(res[1].style).to.eql(styles[1]); + + // Retrieve all inserted rules for the selector. + const rules = FindCss.rules(selector); + expect(rules).to.have.length(2); + + // Verify that each rule is inserted in the DOM, wrapped in the context block. + const expected1 = `${context} { ${selector} { ${toString(styles[0])} } }`; + const expected2 = `${context} { ${selector} { ${toString(styles[1])} } }`; + expect(rules[0].cssText).to.eql(expected1); + expect(rules[1].cssText).to.eql(expected2); + }); + }); + + describe('pseudo-classes', () => { + it('should insert pseudo-class rules along with the base rule', () => { + const { sheet } = setup(); + const selector = '.test-pseudo'; + const style = { + color: 'red', + ':hover': { color: 'green' }, + }; + + // Insert the rule. + sheet.rule(selector, style); + const rules = FindCss.rules(selector); + + // Expect one base rule and one pseudo-class rule. + expect(rules).to.have.length(2); + expect(rules[0].cssText).to.eql(`${selector} { ${toString({ color: 'red' })} }`); + expect(rules[1].cssText).to.eql(`${selector}:hover { ${toString({ color: 'green' })} }`); + }); + + it('should insert multiple pseudo-class rules', () => { + const { sheet } = setup(); + const selector = '.test-multi'; + const style = { + fontSize: '14px', + ':active': { fontSize: '16px' }, + ':focus': { fontWeight: 'bold' }, + }; + + // Insert the rule. + sheet.rule(selector, style); + const rules = FindCss.rules(selector); + + // Expect 1 base rule and 2 pseudo rules. + expect(rules).to.have.length(3); + expect(rules[0].cssText).to.eql(`${selector} { ${toString({ fontSize: '14px' })} }`); + expect(rules[1].cssText).to.eql( + `${selector}:active { ${toString({ fontSize: '16px' })} }`, + ); + expect(rules[2].cssText).to.eql( + `${selector}:focus { ${toString({ fontWeight: 'bold' })} }`, + ); + }); + + it('should ignore pseudo-class rules if the value is not an object', () => { + const test = (invalidValue: any) => { + const { sheet } = setup(); + const selector = `.test-invalid-value-${slug()}`; + const style = { + color: 'blue', + ':hover': invalidValue, // invalid; value must be a {record/object}. + }; + + sheet.rule(selector, style); + const rules = FindCss.rules(selector); + + // Only the base rule should be inserted. + expect(rules).to.have.length(1); + expect(rules[0].cssText).to.eql(`${selector} { ${toString({ color: 'blue' })} }`); + }; + + const NON = ['not-an-object', 123, true, null, undefined, BigInt(0), Symbol('foo'), []]; + NON.forEach(test); + }); + + it('should ignore keys that are not valid pseudo-classes', () => { + const { sheet } = setup(); + const selector = '.test-non-pseudo'; + const style = { + color: 'blue', + ':nonexistent': { color: 'red' }, // not in our DEFAULT pseudo-class set. + }; + + sheet.rule(selector, style); + const rules = FindCss.rules(selector); + + // Only the base rule should be inserted. + expect(rules).to.have.length(1); + expect(rules[0].cssText).to.eql(`${selector} { ${toString({ color: 'blue' })} }`); + }); + + it('should insert an empty pseu0o-class rule when given an empty style object', () => { + const { sheet } = setup(); + const selector = '.test-empty-pseudo'; + const style = { + color: 'blue', + ':hover': {}, // empty nested style. + }; + + sheet.rule(selector, style); + const rules = FindCss.rules(selector); + + // Expect a base rule and a pseudo-class rule, even if the nested style is empty. + expect(rules).to.have.length(2); + expect(rules[0].cssText).to.eql(`${selector} { ${toString({ color: 'blue' })} }`); + expect(rules[1].cssText).to.eql(`${selector}:hover { ${toString({})} }`); + }); + + it('should prevent duplicate pseudo-class rules when the same style is inserted twice', () => { + const { sheet } = setup(); + const selector = '.test-duplicate-pseudo'; + const style = { + color: 'blue', + ':hover': { color: 'red' }, + }; + + // Call rule() twice with the same selector and style. + sheet.rule(selector, style); + sheet.rule(selector, style); + const rules = FindCss.rules(selector); + + // If duplicate prevention is implemented, there should be only one base rule and one pseudo-class rule. + expect(rules).to.have.length(2); + expect(rules[0].cssText).to.eql(`${selector} { ${toString({ color: 'blue' })} }`); + expect(rules[1].cssText).to.eql(`${selector}:hover { ${toString({ color: 'red' })} }`); + }); + }); + }); }, ); diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/-ctx.container.test.ts b/code/sys.ui/ui-css/src/m.Css.Dom/-ctx.container.test.ts new file mode 100644 index 0000000000..921ccc78ea --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/-ctx.container.test.ts @@ -0,0 +1,210 @@ +import { describe, DomMock, expect, FindCss, it, slug, TestPrint } from '../-test.ts'; +import { CssDom } from './mod.ts'; + +const toString = CssDom.toString; + +describe( + 'Stylesheet.container(): scoped @container context', + { sanitizeOps: false, sanitizeResources: false }, // ā because: "Happy-Dom" + () => { + DomMock.polyfill(); + + let _count = 0; + const setup = () => { + _count++; + const sheet = CssDom.stylesheet({ instance: `mysheet-${_count}` }); + return { sheet } as const; + }; + + describe('create', () => { + it('create', () => { + const { sheet } = setup(); + const container = sheet.container(' min-width: 700px '); + TestPrint.container(container); + expect(container.kind).to.eql('@container'); + expect(container.condition).to.eql('(min-width: 700px)'); + expect(container.name).to.eql(undefined); + expect(container.rules.length).to.eql(0); + expect(container.rules.list).to.eql([]); + expect(container.scoped).to.eql([]); + }); + + it("create: cleans up the context's <condition> input", () => { + const { sheet } = setup(); + const test = (condition: string, expected?: string) => { + const container = sheet.container(condition); + expect(container.condition).to.eql(expected ?? condition.trim()); + }; + test(' min-width: 700px ', '(min-width: 700px)'); // NB: parentheses added. + test(' (max-width: 1200px) and (orientation: landscape)'); + }); + + it('create with name', () => { + const { sheet } = setup(); + const container = sheet.container(' card ', ' min-width: 700px '); + TestPrint.container(container); + expect(container.kind).to.eql('@container'); + expect(container.name).to.eql('card'); + expect(container.condition).to.eql('(min-width: 700px)'); + }); + }); + + describe('toString', () => { + it('(default) ā kind: "QueryCondition"', () => { + const { sheet } = setup(); + const a = sheet.container('min-width: 700px'); + const b = sheet.container(' min-width: 700px '); + const c = sheet.container(' my-name ', ' min-width: 700px '); + expect(a.toString()).to.eql('@container (min-width: 700px)'); + expect(b.toString()).to.eql(a.toString()); + expect(c.toString()).to.eql('@container my-name (min-width: 700px)'); + expect(b.toString()).to.eql(b.toString('QueryCondition')); // NB: default kind. + }); + + it('kind: "CssSelector"', () => { + const { sheet } = setup(); + const container = sheet.container('my-name', 'min-width: 700px'); + + const a = container.toString(); + const b = container.toString('CssSelector'); + + container.rules.add('.foo', { fontSize: 16 }); + const c = container.toString('CssSelector'); + container.rules.add('.bar', { color: 'red' }); + const d = container.toString('CssSelector'); + + expect(a).to.eql('@container my-name (min-width: 700px)'); + expect(b).to.eql('@container my-name (min-width: 700px) {}'); + expect(c).to.eql('@container my-name (min-width: 700px) { .foo { font-size: 16px; } }'); + expect(d.split('\n')[0]).to.eql( + '@container my-name (min-width: 700px) { .foo { font-size: 16px; } }', + ); + expect(d.split('\n')[1]).to.eql( + '@container my-name (min-width: 700px) { .bar { color: red; } }', + ); + + TestPrint.container(container); + }); + }); + + describe('adding rules', () => { + it('adds each rule only once', () => { + const { sheet } = setup(); + const styles = [ + { color: 'blue', margin: 10 }, + { backgroundColor: 'yellow', padding: 5 }, + ]; + + const container = sheet.container('min-width: 700px'); + expect(container.rules.list).to.eql([]); + expect(container.rules.length).to.eql(0); + + const selector = `.test-${slug()}`; + const res1 = container.rules.add(selector, styles); + expect(res1.length).to.eql(2); + expect(container.rules.length).to.eql(2); + + // Additional calls with the same style content is not inserted. + expect(container.rules.add(selector, styles)).to.eql([]); // NB: second time - not repeated + expect(container.rules.add(selector, styles[0])).to.eql([]); + expect(container.rules.add(selector, styles[1])).to.eql([]); + expect(container.rules.add(selector, [])).to.eql([]); + + expect(container.rules.list.length).to.eql(2); + expect(container.rules.list[0].selector).to.eql(selector); + expect(container.rules.list[0].style).to.eql(styles[0]); + expect(container.rules.list[1].selector).to.eql(selector); + expect(container.rules.list[1].style).to.eql(styles[1]); + }); + + it('add: CSS template ā { Absolute: 0 }', () => { + const { sheet } = setup(); + const container = sheet.container('min-width: 700px'); + expect(container.rules.list).to.eql([]); + + const selector = `.test-${slug()}`; + const res = container.rules.add(selector, { Absolute: 0 }); + expect(res[0].style).to.eql({ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }); + }); + + it('adds to DOM stylesheet', () => { + const { sheet } = setup(); + const selector = `.test-${slug()}`; + const context = '@container (min-width: 700px)'; + const styles = [ + { color: 'blue', margin: 10 }, + { backgroundColor: 'yellow', padding: 5 }, + ]; + + // Pre-check: Ensure no rule exists for the selector. + expect(FindCss.rules(selector)).to.eql([]); + + const container = sheet.container('min-width: 700px'); + container.rules.add(selector, styles); + + // Retrieve all inserted rules for the selector. + const rules = FindCss.rules(selector); + expect(rules).to.have.length(2); + + // Verify that each rule is inserted in the DOM, wrapped in the context block. + const expected1 = `${context} { ${selector} { ${toString(styles[0])} } }`; + const expected2 = `${context} { ${selector} { ${toString(styles[1])} } }`; + expect(rules[0].cssText).to.eql(expected1); + expect(rules[1].cssText).to.eql(expected2); + }); + + it('scenario: 1', () => { + const { sheet } = setup(); + const container = sheet.container('min-width: 600px'); + sheet.rule('.card h2', { fontSize: 50 }); + container.rules.add('.card h2', { fontSize: 200 }); + expect(container.rules.list[0].rule).to.eql( + `@container (min-width: 600px) { .card h2 { font-size: 200px; } }`, + ); + }); + }); + + describe('scope', () => { + it('nested ".classname" selector', () => { + const { sheet } = setup(); + const a = sheet.container('my-name', 'min-width: 700px'); + const b = a.scope('.foo'); + + expect(b.condition).to.eql(a.condition); + expect(b.name).to.eql(a.name); + expect(b).to.not.equal(a); + + expect(a.scoped).to.eql([]); + expect(b.scoped).to.eql(['.foo']); + + a.rules.add('h1', { color: 'red' }); + b.rules.add('h1', { color: 'red' }); + + expect(a.toString('CssSelector')).to.eql( + '@container my-name (min-width: 700px) { h1 { color: red; } }', + ); + + expect(b.toString('CssSelector')).to.eql( + '@container my-name (min-width: 700px) { .foo h1 { color: red; } }', + ); + }); + + it('multi-level nesting', () => { + const { sheet } = setup(); + const a = sheet.container('min-width: 700px'); + const b = a.scope('.foo'); + const c = b.scope('.bar'); + expect(c.scoped).to.eql(['.foo', '.bar']); + + b.rules.add('h2', { color: 'red' }); + c.rules.add('h2', { color: 'red' }); + + const str1 = b.toString('CssSelector'); + const str2 = c.toString('CssSelector'); + + expect(str1).to.eql('@container (min-width: 700px) { .foo h2 { color: red; } }'); + expect(str2).to.eql('@container (min-width: 700px) { .foo .bar h2 { color: red; } }'); + }); + }); + }, +); diff --git a/code/sys.ui/ui-css/src/m.Style/-toString.test.ts b/code/sys.ui/ui-css/src/m.Css.Dom/-toString.test.ts similarity index 95% rename from code/sys.ui/ui-css/src/m.Style/-toString.test.ts rename to code/sys.ui/ui-css/src/m.Css.Dom/-toString.test.ts index db3db832f9..57a9b63087 100644 --- a/code/sys.ui/ui-css/src/m.Style/-toString.test.ts +++ b/code/sys.ui/ui-css/src/m.Css.Dom/-toString.test.ts @@ -1,5 +1,5 @@ import { type t, describe, expect, it } from '../-test.ts'; -import { toString } from './u.toString.ts'; +import { toString } from './mod.ts'; describe('toString', () => { it('empty', () => { diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/common.ts b/code/sys.ui/ui-css/src/m.Css.Dom/common.ts index b5d899b190..fda62824db 100644 --- a/code/sys.ui/ui-css/src/m.Css.Dom/common.ts +++ b/code/sys.ui/ui-css/src/m.Css.Dom/common.ts @@ -1,17 +1,20 @@ -export * from '../common.ts'; - -export { toHash }; import toHash from 'hash-it'; +import { pixelProps } from './const.pixelProps.ts'; +import { pseudoClasses } from './const.pseudoClasses.ts'; export * from '../common.ts'; export { CssTmpl } from '../m.Css.Tmpl/mod.ts'; -export { toString } from '../m.Style/u.toString.ts'; +export { toHash }; +/** + * Constants. + */ export const DEFAULT = { - prefix: 'sys', + classPrefix: 'sys', get pseudoClasses() { return pseudoClasses; }, + get pixelProps() { + return pixelProps; + }, } as const; - -const pseudoClasses = new Set<string>([':hover']); diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/const.pixelProps.ts b/code/sys.ui/ui-css/src/m.Css.Dom/const.pixelProps.ts new file mode 100644 index 0000000000..3e019bd184 --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/const.pixelProps.ts @@ -0,0 +1,36 @@ +/** + * CSS properties that accept unitless + * numbers (equating to "px" pixels). + */ +export const pixelProps = new Set<string>([ + 'width', + 'height', + 'top', + 'right', + 'bottom', + 'left', + 'margin', + 'marginTop', + 'marginRight', + 'marginBottom', + 'marginLeft', + 'padding', + 'paddingTop', + 'paddingRight', + 'paddingBottom', + 'paddingLeft', + 'borderWidth', + 'borderTopWidth', + 'borderRightWidth', + 'borderBottomWidth', + 'borderLeftWidth', + 'borderRadius', + 'fontSize', + 'minWidth', + 'maxWidth', + 'minHeight', + 'maxHeight', + 'outlineWidth', + 'letterSpacing', + 'wordSpacing', +]); diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/const.pseudoClasses.ts b/code/sys.ui/ui-css/src/m.Css.Dom/const.pseudoClasses.ts new file mode 100644 index 0000000000..0b9e7be587 --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/const.pseudoClasses.ts @@ -0,0 +1,59 @@ +/** + * CSS Selectors Level 3 pseudoāclasses. + * https://www.w3.org/TR/selectors-3 + */ +export const level3PseudoClasses = new Set<string>([ + ':hover', + ':active', + ':focus', + ':visited', + ':link', + ':target', + ':checked', + ':disabled', + ':enabled', + ':first-child', + ':last-child', + ':only-child', + ':nth-child', + ':nth-last-child', + ':first-of-type', + ':last-of-type', + ':only-of-type', + ':empty', + ':root', + ':not', + ':lang', +]); + +/** + * CSS Selectors Level 4 pseudoāclasses (and newer form/structural pseudoāclasses). + * https://www.w3.org/TR/selectors-4 + */ +export const level4PseudoClasses = new Set<string>([ + ':focus-visible', + ':focus-within', + ':any-link', + ':default', + ':indeterminate', + ':in-range', + ':invalid', + ':optional', + ':out-of-range', + ':placeholder-shown', + ':read-only', + ':read-write', + ':required', + ':valid', + ':user-invalid', + ':defined', + ':is', + ':where', + ':has', + ':dir', +]); + +/** + * CSS Pseudo-Classes (Levels 3 & 4). + */ +export const pseudoClasses = new Set<string>([...level3PseudoClasses, ...level4PseudoClasses]); diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/mod.ts b/code/sys.ui/ui-css/src/m.Css.Dom/mod.ts index 263f4f4c71..a8c3453d2b 100644 --- a/code/sys.ui/ui-css/src/m.Css.Dom/mod.ts +++ b/code/sys.ui/ui-css/src/m.Css.Dom/mod.ts @@ -2,9 +2,13 @@ * @module * Tools for programatically managing CSS stylesheets within the browser DOM. */ -import { type t, DEFAULT } from './common.ts'; -import { create } from './u.create.ts'; +import { type t } from './common.ts'; +import { create as stylesheet } from './u.stylesheet.ts'; +import { toString } from './u.toString.ts'; + +export { toString }; export const CssDom: t.CssDomLib = { - create, + stylesheet, + toString, }; diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/t.ctx.ts b/code/sys.ui/ui-css/src/m.Css.Dom/t.ctx.ts new file mode 100644 index 0000000000..12f801b1c6 --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/t.ctx.ts @@ -0,0 +1,37 @@ +import type { t } from './common.ts'; + +/** + * Represents a CSS/DOM context-block that encapsulates a set of CSS rules + * applied within a @container context. + */ +export type CssDomContainerBlock = { + /** The type of the context-block. */ + readonly kind: '@container'; + + /** The conditional rules for the context block, eg "min-width: 700px". */ + readonly condition: string; + + /** The name of the container (empty if unnamed)/ */ + readonly name?: string; + + /** Raw rule API. */ + readonly rules: { + /** The total number of inserted rules. */ + readonly length: number; + /** List of inserted rules wihtin the container. */ + readonly list: Readonly<t.CssDomInsertedRule[]>; + /** Inserts CSS styles with the given selector within a context-block. */ + add(selector: t.StringCssSelector, style: t.CssValue | t.CssValue[]): t.CssDomInsertedRule[]; + }; + + /** String representation of the block. */ + toString(kind?: t.CssDomContainerToStringKind): string; + + /** Creates a scoped sub-block prefixing the child rules with the given selector. */ + scope(selector: t.StringCssSelector): CssDomContainerBlock; + /** The list of CSS selectors that represent the scope this container is within. */ + readonly scoped: Readonly<t.StringCssSelector[]>; +}; + +/** Flags indicating the kind of string to export from the `toString` method. */ +export type CssDomContainerToStringKind = 'QueryCondition' | 'CssSelector'; diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/t.ts b/code/sys.ui/ui-css/src/m.Css.Dom/t.ts index c1de12bac2..28b80a29e2 100644 --- a/code/sys.ui/ui-css/src/m.Css.Dom/t.ts +++ b/code/sys.ui/ui-css/src/m.Css.Dom/t.ts @@ -1,24 +1,121 @@ import type { t } from './common.ts'; +export type * from './t.ctx.ts'; /** * Tools for programatically managing CSS stylesheets within the browser DOM. */ export type CssDomLib = { - /** Generator factory. */ - create(prefix?: string): t.CssDom; + /** Factory for a DOM <style> stylesheet element (singleton instances). */ + stylesheet(options?: CssDomStylesheetOptions | t.StringId): t.CssDomStylesheet; + + /** Convert a {style} props object to a CSS string. */ + toString: t.StyleLib['toString']; }; +/** Options passed to the `Style.Dom.stylesheet` method. */ +export type CssDomStylesheetOptions = { instance?: t.StringId; classPrefix?: string }; + /** * A <style> DOM element used to store and manage CSS-classes * generated from CssProps */ -export type CssDom = { - /** The root prefix applied to generated class-names. */ +export type CssDomStylesheet = { + readonly id: t.StringId; + + /** + * Inserts CSS style rules into the stylesheet. + * Accepts either a single style object or an array of style objects. + * Optionally, a context string can be provided to wrap the rules. + * + * Example: + * + * api.add(".card h2", { fontSize: "2em" }, { context: "@container (min-width: 700px)" }); + * + * // or multiple style objects: + * api.add(".card", [ + * { color: "blue" }, + * { margin: "1rem", ":hover": { color: "red" } } + * ], { context: "@media (min-width: 600px)" }); + */ + rule( + selector: string, + style: t.CssValue | t.CssValue[], + options?: CssDomRuleOptions, + ): CssDomInsertedRule[]; + /** Rules API. */ + readonly rules: t.CssDomRules; + + /** + * Retrieve the singleton instance of the classes API + * with the given classname "prefix". + */ + classes(prefix?: string): t.CssDomClasses; + + /** + * Retrieve the singleton instance of an @container API + * for specifying style rules within a specific size container. + */ + container(condition: string): t.CssDomContainerBlock; + container(name: string, condition: string): t.CssDomContainerBlock; +}; + +/** + * API for inserting CSS class-styes into a DOM's stylesheet. + */ +export type CssDomClasses = { + /** The root prefix applied to generated class-names: "<prefix>-<hash>". */ readonly prefix: string; - /** List of CSS class-names that have been inserted into the DOM. */ - readonly classes: Readonly<string[]>; + /** List of CSS class-names that have been inserted into the DOM. */ + readonly names: Readonly<string[]>; + + /** + * Generates a CSS classname as the selector and inserts the given + * {Style} object as a set of rules into the DOM (with caching). + */ + add(style: t.CssValue, options?: { hx?: number }): string; +}; + +/** + * API for inserting CSS rules into a DOM's stylesheet. + */ +export type CssDomRules = { + /** The total number of inserted rules. */ + readonly length: number; + + /** List of CSS rules that have been inserted into the DOM. */ + readonly list: Readonly<CssDomInsertedRule[]>; + + /** + * Inserts generic CSS style rules into the stylesheet. + * Accepts either a single style object or an array of style objects. + * Optionally, a context string can be provided to wrap the rules. + * + * Example: + * + * api.add(".card h2", { fontSize: "2em" }, { context: "@container (min-width: 700px)" }); + * + * // or multiple style objects: + * api.add(".card", [ + * { color: "blue" }, + * { margin: "1rem", ":hover": { color: "red" } } + * ], { context: "@media (min-width: 600px)" }); + * + * @returns true if inserted, or false if already inserted. + */ + add( + selector: string, + style: t.CssValue | t.CssValue[], + options?: CssDomRuleOptions, + ): CssDomInsertedRule[]; +}; + +/** Options passed to the rule insertion method. */ +export type CssDomRuleOptions = { context?: string }; - /** Generates a CSS class-name and inserts the given {Style} into the DOM. */ - class(style: t.CssProps, hx?: number): string; +/** The receipt of a rule that has been inserted into the DOM. */ +export type CssDomInsertedRule = { + readonly selector: string; + readonly style: t.CssProps; + readonly rule: string; }; diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/u.classes.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.classes.ts new file mode 100644 index 0000000000..fb47d6fbe2 --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/u.classes.ts @@ -0,0 +1,37 @@ +import { type t, DEFAULT, toHash, V } from './common.ts'; +import { AlphanumericWithHyphens } from './u.ts'; + +export function createClasses(args: { rules: t.CssDomRules; prefix?: string }): t.CssDomClasses { + const { rules } = args; + const inserted = new Set<string>(); + const prefix = wrangleClassPrefix(args.prefix); + V.parse(AlphanumericWithHyphens, prefix); + + const api: t.CssDomClasses = { + prefix, + get names() { + return Array.from(inserted); + }, + add(style, options = {}) { + const hx = options.hx ?? toHash(style); + const className = `${prefix}-${hx}`; + if (inserted.has(className)) { + return className; + } else { + rules.add(`.${className}`, style); + inserted.add(className); + return className; + } + }, + }; + + return api; +} + +/** + * Helpers + */ +export function wrangleClassPrefix(input: string | undefined, defaultPrefix?: string) { + const res = (input ?? '').trim() || (defaultPrefix ?? DEFAULT.classPrefix); + return res.replace(/^\.*/, '').replace(/-*$/, ''); +} diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/u.create.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.create.ts deleted file mode 100644 index ec9b1acdff..0000000000 --- a/code/sys.ui/ui-css/src/m.Css.Dom/u.create.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type t, DEFAULT, isRecord, pkg, toHash, toString, V } from './common.ts'; -import { AlphanumericWithHyphens } from './u.ts'; - -type Prefix = string; -const singletons = new Map<Prefix, t.CssDom>(); -let _sheet: CSSStyleSheet | null = null; - -/** - * Generator factory - */ -export const create: t.CssDomLib['create'] = (prefix) => { - prefix = ((prefix ?? '').trim() || DEFAULT.prefix).replace(/-*$/, ''); - V.parse(AlphanumericWithHyphens, prefix); - if (singletons.has(prefix)) return singletons.get(prefix)!; - - const sheet = getOrCreateStylesheet(); - const inserted = new Set<string>(); - const insertRule = (rule: string) => sheet.insertRule?.(rule, sheet.cssRules.length); - - const api: t.CssDom = { - prefix, - get classes() { - return Array.from(inserted); - }, - class(style, hxInput) { - const hx = hxInput ?? toHash(style); - const className = `${prefix}-${hx}`; - if (inserted.has(className)) return className; - - // Initial creation. - inserted.add(className); - insertRule(`.${className} { ${toString(style)} }`); - - Object.entries(style) - .filter(([key]) => DEFAULT.pseudoClasses.has(key)) - .filter(([_, value]) => isRecord(value)) - .forEach(([key, value]) => insertRule(`.${className}${key} { ${toString(value)} }`)); - - return className; - }, - }; - - singletons.set(prefix, api); - return api; -}; - -/** - * Helpers - */ - -/** - * Singleton <style> element management. - * If one doesn't exist, we create one and append it to the <head>. - */ -function getOrCreateStylesheet(): CSSStyleSheet { - if (_sheet) return _sheet; - - if (typeof document === 'undefined') { - return {} as CSSStyleSheet; // Dummy (NB: safe when running on server). - } else { - const el = document.createElement('style'); - el.setAttribute('data-controller', pkg.name); - document.head.appendChild(el); - _sheet = el.sheet as CSSStyleSheet; - return _sheet; - } -} diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/u.ctx.container.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.ctx.container.ts new file mode 100644 index 0000000000..d7ef4606da --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/u.ctx.container.ts @@ -0,0 +1,85 @@ +import { type t } from './common.ts'; + +/** + * CSS: @container API + */ +export function createContainer(args: { + rules: t.CssDomRules; + condition: string; + name?: string; + scoped?: t.StringCssSelector[]; +}): t.CssDomContainerBlock { + const { rules, name, scoped = [] } = args; + const condition = wrangle.condition(args.condition); + const inserted = new Set<t.CssDomInsertedRule>(); + + const api: t.CssDomContainerBlock = { + kind: '@container', + condition, + name, + toString(kind = 'QueryCondition') { + return toString(api, kind); + }, + rules: { + get length() { + return inserted.size; + }, + get list() { + return Array.from(inserted); + }, + add(selector, style) { + selector = wrangle.selector(selector, scoped); + const context = api.toString(); + const styles = Array.isArray(style) ? style : [style]; + const res = rules.add(selector, styles, { context }); + if (res.length > 0) res.forEach((m) => inserted.add(m)); + return res; + }, + }, + scoped, + scope(selector) { + return createContainer({ rules, name, condition, scoped: [...scoped, selector] }); + }, + }; + + return api; +} + +/** + * Convert a container block to a string. + */ +export function toString( + container: t.CssDomContainerBlock, + kind: t.CssDomContainerToStringKind = 'QueryCondition', +): string { + if (kind === 'QueryCondition') { + let res = container.kind; + if (container.name) res += ` ${container.name}`; + res += ` ${container.condition}`; + return res; + } + + if (kind === 'CssSelector') { + const rules = container.rules.list; + if (rules.length === 0) return `${toString(container, 'QueryCondition')} {}`; + return rules.map(({ rule }) => rule).join('\n'); + } + + throw new Error(`toString kind value "${kind}" not supported`); +} + +/** + * Helpers + */ +const wrangle = { + condition(text: string): string { + text = (text || '').trim(); + if (!text.includes('(')) text = `(${text}`; + if (!text.includes(')')) text = `${text})`; + return text; + }, + + selector(selector: string, scope: t.StringCssSelector[]) { + return `${scope.join(' ')} ${selector}`.trim(); + }, +} as const; diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/u.rules.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.rules.ts new file mode 100644 index 0000000000..ef7d90506f --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/u.rules.ts @@ -0,0 +1,58 @@ +import { type t, CssTmpl, DEFAULT, isRecord } from './common.ts'; +import { toString } from './u.ts'; +type StringRule = string; + +export function createRules(args: { sheet: CSSStyleSheet }): t.CssDomRules { + const { sheet } = args; + const inserted = new Map<StringRule, t.CssDomInsertedRule>(); + + const insert = ( + selector: string, + style: t.CssProps, + context?: string, + ): t.CssDomInsertedRule | undefined => { + let rule = `${selector.trim()} { ${toString(style)} }`.trim(); + rule = context ? `${context} { ${rule} }` : rule; + if (inserted.has(rule)) return undefined; + + const res: t.CssDomInsertedRule = { selector, rule, style }; + sheet.insertRule?.(rule, sheet.cssRules.length); + inserted.set(rule, res); + return res; + }; + + const addRule = ( + selector: string, + style: t.CssValue, + context?: string, + ): t.CssDomInsertedRule[] => { + const res: (t.CssDomInsertedRule | undefined)[] = []; + res.push(insert(selector, CssTmpl.transform(style), context)); + Object.entries(style) + .filter(([key]) => DEFAULT.pseudoClasses.has(key)) + .filter(([, value]) => isRecord(value)) + .forEach(([key, style]) => res.push(insert(`${selector}${key}`, style, context))); + return res.filter(Boolean) as t.CssDomInsertedRule[]; + }; + + const api: t.CssDomRules = { + get length() { + return inserted.size; + }, + get list() { + return Array.from(inserted.values()); + }, + + add(selector, styles, options = {}) { + const res: t.CssDomInsertedRule[] = []; + const { context } = options; + const list = Array.isArray(styles) ? styles : [styles]; + list.forEach((style) => { + res.push(...addRule(selector, style, context)); + }); + return res; + }, + }; + + return api; +} diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/u.stylesheet.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.stylesheet.ts new file mode 100644 index 0000000000..35570495ba --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Css.Dom/u.stylesheet.ts @@ -0,0 +1,83 @@ +import { type t } from './common.ts'; +import { createClasses, wrangleClassPrefix } from './u.classes.ts'; +import { createContainer } from './u.ctx.container.ts'; +import { createRules } from './u.rules.ts'; +import { getStylesheetId } from './u.ts'; + +const singletons = new Map<t.StringId, t.CssDomStylesheet>(); + +/** + * Generator factory + */ +export const create: t.CssDomLib['stylesheet'] = (input) => { + const options = wrangle.options(input); + const id = getStylesheetId(options.instance, options.classPrefix); + if (singletons.has(id)) return singletons.get(id)!; + + const sheet = getOrCreateDomStyleSheet(id); + const rules = createRules({ sheet }); + const cache = { + classes: new Map<string, t.CssDomClasses>(), + getOrCreate<T>(key: string, map: Map<string, T>, factory: () => T): T { + if (!map.has(key)) map.set(key, factory()); + return map.get(key)!; + }, + }; + + const api: t.CssDomStylesheet = { + id, + rules, + rule(selector, style, options) { + return rules.add(selector, style, options); + }, + classes(prefix) { + const key = wrangleClassPrefix(prefix, options.classPrefix); + prefix = prefix ?? options.classPrefix; + return cache.getOrCreate(key, cache.classes, () => createClasses({ rules, prefix })); + }, + container(...args: any[]) { + const { name, condition } = wrangle.containerArgs(args); + return createContainer({ rules, condition, name }); + }, + }; + + singletons.set(id, api); + return api; +}; + +/** + * Helpers: + */ +const wrangle = { + options(input: Parameters<t.CssDomLib['stylesheet']>[0]): t.CssDomStylesheetOptions { + if (!input) return {}; + if (typeof input === 'string') return { instance: input }; + return input; + }, + + containerArgs(args: any[]) { + const done = (condition: string, name?: string) => { + name = name ? name.trim() : name; + condition = condition ? condition.trim() : ''; + return { name, condition }; + }; + if (!args || args.length === 0) return done(''); + if (args.length === 1) return done(args[0]); + return done(args[1], args[0]); + }, +} as const; + +/** + * Singleton <style> element management. + * If one doesn't exist, we create one and append it to the <head>. + */ +function getOrCreateDomStyleSheet(id: string): CSSStyleSheet { + if (typeof document === 'undefined') { + return {} as CSSStyleSheet; // Dummy (safe on server). + } else { + const el = document.createElement('style'); + el.setAttribute('data-controller', id); + document.head.appendChild(el); + return el.sheet as CSSStyleSheet; + } +} diff --git a/code/sys.ui/ui-css/src/m.Style/u.toString.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.toString.ts similarity index 100% rename from code/sys.ui/ui-css/src/m.Style/u.toString.ts rename to code/sys.ui/ui-css/src/m.Css.Dom/u.toString.ts diff --git a/code/sys.ui/ui-css/src/m.Css.Dom/u.ts b/code/sys.ui/ui-css/src/m.Css.Dom/u.ts index c91e4781e5..ad5e463443 100644 --- a/code/sys.ui/ui-css/src/m.Css.Dom/u.ts +++ b/code/sys.ui/ui-css/src/m.Css.Dom/u.ts @@ -1,4 +1,13 @@ -import { V } from './common.ts'; +import { V, pkg } from './common.ts'; +export { toString } from './u.toString.ts'; + +export function getStylesheetId(instance?: string, defaultPrefix?: string) { + instance = (instance || '').trim(); + let id = pkg.name; + if (instance) id += `:${instance}`; + if (defaultPrefix) id += `:${defaultPrefix}`; + return id; +} /** * Validation. diff --git a/code/sys.ui/ui-css/src/m.Css.Tmpl/mod.ts b/code/sys.ui/ui-css/src/m.Css.Tmpl/mod.ts index 2f7fc234b6..27b0ffa3fc 100644 --- a/code/sys.ui/ui-css/src/m.Css.Tmpl/mod.ts +++ b/code/sys.ui/ui-css/src/m.Css.Tmpl/mod.ts @@ -8,6 +8,9 @@ import { formatSize } from './u.formatSize.ts'; import { toEdges, WrangleEdge } from './u.toEdges.ts'; import { formatScroll } from './u.formatScroll.ts'; +/** + * Helpers for working with the template patterns (a DSL for css of sorts). + */ export const CssTmpl: t.CssTmplLib = { toEdges, diff --git a/code/sys.ui/ui-css/src/m.Css.Tmpl/t.ts b/code/sys.ui/ui-css/src/m.Css.Tmpl/t.ts index bf4ac7d292..e34a8f853b 100644 --- a/code/sys.ui/ui-css/src/m.Css.Tmpl/t.ts +++ b/code/sys.ui/ui-css/src/m.Css.Tmpl/t.ts @@ -1,7 +1,7 @@ import type { t } from './common.ts'; /** - * Helpers for working with the [CssTemplates] DSL. + * Helpers for working with the template patterns (a DSL for css of sorts). */ export type CssTmplLib = { /** diff --git a/code/sys.ui/ui-css/src/m.Style/-.test.ts b/code/sys.ui/ui-css/src/m.Style/-.test.ts index dca74928ff..eff29a3173 100644 --- a/code/sys.ui/ui-css/src/m.Style/-.test.ts +++ b/code/sys.ui/ui-css/src/m.Style/-.test.ts @@ -49,12 +49,13 @@ describe('Style', () => { it('Ęn: transformer ā <default> class-name prefix', () => { const css = Style.transformer(); const m = css({ display: 'grid' }); - expect(m.class.startsWith(`${DEFAULT.prefix}-`)).to.be.true; + expect(m.class.startsWith(`${DEFAULT.classPrefix}-`)).to.be.true; TestPrint.transformed(m); }); it('Ęn: transformer ā <custom> class-name prefix', () => { - const css = Style.transformer({ prefix: 'foo' }); + const sheet = Style.Dom.stylesheet({ classPrefix: 'foo' }); + const css = Style.transformer({ sheet }); const m = css({ display: 'grid' }); expect(m.class.startsWith(`foo-`)).to.be.true; TestPrint.transformed(m); diff --git a/code/sys.ui/ui-css/src/m.Style/-css.transform.test.ts b/code/sys.ui/ui-css/src/m.Style/-css.transform.test.ts new file mode 100644 index 0000000000..ed5f2b1401 --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Style/-css.transform.test.ts @@ -0,0 +1,386 @@ +import { type t, DomMock, FindCss, TestPrint, c, describe, expect, it, slug } from '../-test.ts'; +import { toHash } from './common.ts'; +import { Style, css } from './mod.ts'; + +describe( + 'Style.css ā transform', + + /** NB: leaked timers left around by the "happy-dom" module. */ + { sanitizeOps: false, sanitizeResources: false }, + + () => { + DomMock.polyfill(); + + const setup = () => { + const sheet = Style.Dom.stylesheet(slug()); + const css = Style.transformer({ sheet }); + return { sheet, css } as const; + }; + + it('API', () => { + expect(Style.css).to.equal(css); + }); + + describe('css ā { styles }', () => { + it('empty', () => { + const a = css(); + const b = css([]); + const c = css(...[], false); + const d = css(null, undefined, [], false, ...[]); + + expect(a.style).to.eql({}); + expect(b).to.eql(a); + expect(c).to.eql(a); + expect(d).to.eql(a); + + // Cached instances (on hx). + expect(a.style).to.equal(b.style); + expect(a.style).to.equal(c.style); + expect(a.style).to.equal(d.style); + }); + + it('plain CSS fields', () => { + const a = css({ fontSize: 30 }); + const b = css({ fontSize: 30 }); + const c = css({ fontSize: 31 }); + + expect(a.style.fontSize).to.eql(30); + expect(b.style.fontSize).to.eql(30); + expect(c.style.fontSize).to.eql(31); + + expect(a).to.equal(b); + expect(a).to.not.equal(c); + }); + }); + + describe('{ Template } ā {styles} | known templates starting with a capital-letter', () => { + it('Absolute: 0', () => { + const a = css({ Absolute: 0 }); + const b = { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }; + expect(a.style).to.eql(b); + expect(a.hx).to.eql(toHash(b)); + expect((a.style as any).Absolute).to.eql(undefined); // NB: clean up on object after transform. + }); + + it('PaddingX: [10, 20]', () => { + const a = css({ PaddingX: [10, 20] }); + const b = { paddingLeft: 10, paddingRight: 20 }; + expect(a.style).to.eql(b); + expect(a.hx).to.eql(toHash(b)); + expect((a.style as any).PaddingX).to.eql(undefined); // NB: clean up on object after transform. + }); + }); + + describe('css ā "class-name" (inserted into DOM)', () => { + it('simple', () => { + const sheet = Style.Dom.stylesheet({ classPrefix: 'foo' }); + const css = Style.transformer({ sheet }); + + const input: t.CssValue = { PaddingX: [10, 30] }; + const m = css({ PaddingX: [10, 30] }); + const className = `foo-${m.hx}`; + expect(FindCss.rule(className)).to.eql(undefined); + + console.info(c.gray('\nInput:'), input); + console.info(c.gray('ā')); + TestPrint.transformed(m); + + expect(m.class).to.eql(className); // NB: insertion into DOM happens here. + expect(FindCss.rule(className)?.cssText).to.include(className); + expect(FindCss.rule(className)?.cssText).to.include(m.toString()); + }); + }); + + describe('toString', () => { + const style = { fontSize: 30, fontFamily: 'sans-serif' }; + + const print = (kind: t.CssTransformToStringKind, value: string) => { + console.info(); + console.info(`${c.brightCyan(kind)}: "${c.yellow(value)}"`); + console.info(); + }; + + it('empty', () => { + expect(css({}).toString()).to.eql(''); + }); + + it('kind: CssRule (default)', () => { + const m = css(style); + const a = m.toString(); + const b = m.toString('CssRule'); + + print('CssRule', m.toString()); + + expect(a).to.eql('font-size: 30px; font-family: sans-serif;'); + expect(a).to.eql(b); + }); + + it('kind: CssSelector', () => { + const sheet = Style.Dom.stylesheet({ classPrefix: 'foo' }); + const css = Style.transformer({ sheet }); + const m = css(style); + const str = m.toString('CssSelector'); + + print('CssSelector', str); + expect(str).to.eql(`.foo-${m.hx} { font-size: 30px; font-family: sans-serif; }`); + }); + }); + + describe('merging', () => { + it('basic merge', () => { + const a = css({ color: 'red' }); + const b = css({ background: 'blue' }); + + const res = css(a, b); + expect(res.style).to.include({ color: 'red' }); + expect(res.style).to.include({ background: 'blue' }); + }); + + it('deep merge', () => { + const assert = (res: t.CssTransformed) => { + expect(res.style).to.include({ color: 'red' }); + expect(res.style).to.include({ background: 'blue' }); + }; + + const props = { style: { color: 'red' } }; + const styles = { base: css({ background: 'blue' }) }; + + assert(css(styles.base, props.style)); + assert(css(styles.base, css(props.style))); + assert(css(css(styles.base), css(css(props.style)))); + assert(css([css(styles.base), css(css(props.style))])); + assert( + css( + css([css(styles.base), css(css(props.style))]), + css([css(styles.base), [css(css([css([[props.style]])]))]]), + ), + ); + }); + + it('deep merge ā {style} object', () => { + const assert = (res: t.CssTransformed) => { + expect(res.style).to.include({ color: 'red' }); + expect(res.style).to.include({ background: 'blue' }); + }; + + const props = { style: { color: 'red' } }; + const styles = { base: css({ background: 'blue' }) }; + + assert(css(styles.base, props.style)); + assert(css(styles.base, css(props.style))); + assert(css(css(styles.base), css(css(props.style)))); + assert(css([css(styles.base), css(css(props.style))])); + assert( + css( + css([css(styles.base), Style.css(css(props.style))]), + css([css(styles.base), [Style.css(css([css([[props.style]])]))]]), + ), + ); + }); + + it('cache name repeats (reused)', () => { + const props = { style: { color: 'red' } }; + const styles = { base: css({ background: 'blue' }) }; + + const a = css(css(styles.base), css(css(props.style))); + const b = css(css(styles.base), css(css(props.style))); + expect(a.hx).to.equal(b.hx); + }); + + it('wrapped ā equality', () => { + const a = css({ color: 'red' }); + const b = css(a); + const c = css([b]); + const d = css([a, c], b); + expect(b).to.eql(a); + expect(c).to.eql(a); + expect(d).to.eql(a); + }); + }); + + describe('.container', () => { + it('scoped with root class-name', () => { + const a = css({ Absolute: 0 }); + const b = a.container('min-width: 500px'); + expect(b.block.kind).to.eql('@container'); + expect(b.block.scoped).to.eql([`.${a.class}`]); + }); + + it('with {style} param', () => { + const condition = 'min-width: 500px'; + const style = { Absolute: 0, fontSize: 42 }; + const base = css({ Absolute: 0 }); + + const a = base.container(condition); + const b = base.container(condition, style); + const c = base.container('my-name', condition); + const d = base.container('my-name', condition, style); + + [a, b, c, d].forEach((m) => { + expect(m.block.kind).to.eql('@container'); + expect(m.block.condition).to.eql(`(${condition})`); + }); + + [a, b].forEach((m) => expect(m.block.name).to.eql(undefined)); + expect(c.block.name).to.eql('my-name'); + expect(d.block.name).to.eql('my-name'); + + [a, c].forEach((m) => expect(m.block.rules.length).to.eql(0)); + [b, d].forEach((m) => { + expect(m.block.rules.length).to.eql(1); + expect(m.block.rules.list[0].style).to.eql({ + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + fontSize: 42, + }); + }); + }); + + it('nest: multi-level descendents', () => { + const a = css({ Absolute: 0 }); + const b = a.container('min-width: 500px'); + const c = b.nest('h2'); + const d = c.nest('code'); + [b, c, d].forEach(({ block }) => expect(block.kind).to.eql('@container')); + expect(c.block.scoped).to.eql([`.${a.class}`, 'h2']); + expect(d.block.scoped).to.eql([`.${a.class}`, 'h2', 'code']); + }); + + it('add custom selector: .rule()', () => { + const container = css({ Absolute: 0 }).container('min-width: 500px'); + const rules = container.block.rules; + expect(rules.length).to.eql(0); + + const styles = [{ color: 'red' }, { fontSize: 32 }, { color: 'blue' }]; + const a = container.rule('h2', styles[0]); + const b = container.rule('h2', [styles[1], styles[2]]); + + expect(a[0].style).to.eql(styles[0]); + expect(b[0].style).to.eql(styles[1]); + expect(b[1].style).to.eql(styles[2]); + + expect(rules.length).to.eql(3); + + const root = container.block.scoped[0]; + rules.list.forEach(({ rule }) => expect(rule).to.include(`{ ${root} h2 {`)); + }); + + it('add custom selector: .rule() ā CSS template', () => { + const container = css({ Absolute: 0 }).container('min-width: 500px'); + const rules = container.block.rules; + expect(rules.length).to.eql(0); + + const res = container.rule('h2', { Absolute: 0 }); + expect(res[0].style).to.eql({ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }); + expect(rules.length).to.eql(1); + }); + + it('add rule: .css()', () => { + const container = css({ Absolute: 0 }).container('min-width: 500px'); + const root = container.block.scoped[0]; + const rules = container.block.rules; + expect(rules.length).to.eql(0); + + const a = container.css({ color: 'red' }); + expect(a).to.equal(container); + expect(rules.length).to.eql(1); + expect(rules.list[0].rule).to.include(`{ ${root} { color: red; } }`); + + container.css([{ PaddingX: 10 }, { color: 'blue' }]); // NB: CSS template expansion. + expect(rules.length).to.eql(3); + + expect(rules.list[1].rule).to.include(`${root} { padding-right: 10px; padding-left: 10px`); + expect(rules.list[2].rule).to.include(`{ ${root} { color: blue; } }`); + }); + + it('done (property): end a fluent API chain', () => { + const base = css({ Absolute: 0 }); + const container = base.container('min-width: 500px'); + expect(container.done).to.equal(base); + expect(container.css({ color: 'red' }).done).to.eql(base); + expect(container.nest('h2').css({ color: 'red' }).done).to.eql(base); + }); + + it('sample: fluent chaining', () => { + const { sheet, css } = setup(); + expect(sheet.rules.length).to.eql(0); + + const styles = { + base: css({ containerType: 'size' }), + h2: css({ fontSize: 50 }) + .container('min-width: 400px', { fontSize: 100 }) + .container('min-width: 700px', { PaddingX: 10 }).done, + }; + + // NB: flush rules. + styles.base.class; + styles.h2.class; + + const list = sheet.rules.list.map((m) => m.style); + expect(list).to.eql([ + { fontSize: 50 }, + { fontSize: 100 }, + { paddingLeft: 10, paddingRight: 10 }, // NB: expanded CSS template (PaddingX) + { containerType: 'size' }, + ]); + }); + }); + + describe('.rule (arbitrary sub-selectors)', () => { + it('nests the sub-selector', () => { + const { sheet, css } = setup(); + const base = css({ position: 'relative' }); + expect(sheet.rules.length).to.eql(0); + + const a = base.rule('h2', { color: 'red' }); + const b = base.rule('h2', { color: 'red' }); // NB: not-added (duplicate). + + expect(a).to.equal(base); // NB: enabled API chaining ("fluent"). + expect(a).to.equal(b); + + const rules = sheet.rules.list; + expect(rules.length).to.eql(2); + expect(rules[0].rule).to.include(`.${base.class} { position: relative; }`); + expect(rules[1].rule).to.include(`.${base.class} h2 { color: red; }`); + }); + + it('sample: chaining', () => { + const { sheet, css } = setup(); + const styles = { + base: css({ position: 'relative' }) + .rule('h1', { color: 'red' }) + .rule('h2', { color: 'blue' }) + .rule('h2 code', { color: 'green' }), + }; + + const baseClass = styles.base.class; + const rules = sheet.rules.list; + + expect(rules.length).to.eql(4); + expect(rules[0].rule).to.include(`.${baseClass} { position: relative; }`); + expect(rules[1].rule).to.include(`.${baseClass} h1 { color: red; }`); + expect(rules[2].rule).to.include(`.${baseClass} h2 { color: blue; }`); + expect(rules[3].rule).to.include(`.${baseClass} h2 code { color: green; }`); + }); + + it('empty selector', () => { + const { sheet, css } = setup(); + + const base = css({ position: 'relative' }); + expect(sheet.rules.length).to.eql(0); + + base.rule('', { color: 'red' }); + base.rule(' ', { color: 'red' }); // NB: not-added (trimmed ā duplicate). + base.rule(' ', { color: 'red' }); + + const rules = sheet.rules.list; + expect(rules.length).to.eql(2); + expect(rules[0].rule).to.include(`.${base.class} { position: relative; }`); + expect(rules[1].rule).to.include(`.${base.class} { color: red; }`); + }); + }); + }, +); diff --git a/code/sys.ui/ui-css/src/m.Style/-transform.test.ts b/code/sys.ui/ui-css/src/m.Style/-transform.test.ts deleted file mode 100644 index 0f8d5c0ccf..0000000000 --- a/code/sys.ui/ui-css/src/m.Style/-transform.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { type t, DomMock, FindCss, TestPrint, c, describe, expect, it } from '../-test.ts'; -import { toHash } from './common.ts'; -import { Style, css } from './mod.ts'; - -describe( - 'Style.css ā transform', - - /** NB: leaked timers left around by the "happy-dom" module. */ - { sanitizeOps: false, sanitizeResources: false }, - - () => { - DomMock.polyfill(); - - it('API', () => { - expect(Style.css).to.equal(css); - }); - - describe('css ā { styles }', () => { - it('empty', () => { - const a = css(); - const b = css([]); - const c = css(...[], false); - const d = css(null, undefined, [], false, ...[]); - - expect(a.style).to.eql({}); - expect(b).to.eql(a); - expect(c).to.eql(a); - expect(d).to.eql(a); - - // Cached instances (on hx). - expect(a.style).to.equal(b.style); - expect(a.style).to.equal(c.style); - expect(a.style).to.equal(d.style); - }); - - it('plain CSS fields', () => { - const a = css({ fontSize: 30 }); - const b = css({ fontSize: 30 }); - const c = css({ fontSize: 31 }); - - expect(a.style.fontSize).to.eql(30); - expect(b.style.fontSize).to.eql(30); - expect(c.style.fontSize).to.eql(31); - - expect(a).to.equal(b); - expect(a).to.not.equal(c); - }); - }); - - describe('{ Template } ā {styles} | known templates starting with a capital-letter', () => { - it('Absolute: 0', () => { - const a = css({ Absolute: 0 }); - const b = { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }; - expect(a.style).to.eql(b); - expect(a.hx).to.eql(toHash(b)); - expect((a.style as any).Absolute).to.eql(undefined); // NB: clean up on object after transform. - }); - - it('PaddingX: [10, 20]', () => { - const a = css({ PaddingX: [10, 20] }); - const b = { paddingLeft: 10, paddingRight: 20 }; - expect(a.style).to.eql(b); - expect(a.hx).to.eql(toHash(b)); - expect((a.style as any).PaddingX).to.eql(undefined); // NB: clean up on object after transform. - }); - }); - - describe('css ā "class-name" (inserted into DOM)', () => { - it('simple', () => { - const prefix = 'foo'; - const css = Style.transformer({ prefix }); - - const input: t.CssValue = { PaddingX: [10, 30] }; - const m = css({ PaddingX: [10, 30] }); - const className = `${prefix}-${m.hx}`; - expect(FindCss.rule(className)).to.eql(undefined); - - console.info(c.gray('\nInput:'), input); - console.info(c.gray('ā')); - TestPrint.transformed(m); - - expect(m.class).to.eql(className); // NB: insertion into DOM happens here. - expect(FindCss.rule(className)?.cssText).to.include(className); - expect(FindCss.rule(className)?.cssText).to.include(m.toString()); - }); - }); - - describe('toString', () => { - const style = { fontSize: 30, fontFamily: 'sans-serif' }; - - const print = (kind: t.CssTransformStringKind, value: string) => { - console.info(); - console.info(`${c.brightCyan(kind)}: "${c.yellow(value)}"`); - console.info(); - }; - - it('empty', () => { - expect(css({}).toString()).to.eql(''); - }); - - it('kind: CssRule (default)', () => { - const m = css(style); - const a = m.toString(); - const b = m.toString('CssRule'); - - print('CssRule', m.toString()); - - expect(a).to.eql('font-size: 30px; font-family: sans-serif;'); - expect(a).to.eql(b); - }); - - it('kind: CssSelector', () => { - const css = Style.transformer('foo'); - const m = css(style); - const str = m.toString('CssSelector'); - - print('CssSelector', str); - expect(str).to.eql(`.foo-${m.hx} { font-size: 30px; font-family: sans-serif; }`); - }); - }); - - describe('merging', () => { - it('basic merge', () => { - const a = css({ color: 'red' }); - const b = css({ background: 'blue' }); - - const res = css(a, b); - expect(res.style).to.include({ color: 'red' }); - expect(res.style).to.include({ background: 'blue' }); - }); - - it('deep merge', () => { - const assert = (res: t.CssTransformed) => { - expect(res.style).to.include({ color: 'red' }); - expect(res.style).to.include({ background: 'blue' }); - }; - - const props = { style: { color: 'red' } }; - const styles = { base: css({ background: 'blue' }) }; - - assert(css(styles.base, props.style)); - assert(css(styles.base, css(props.style))); - assert(css(css(styles.base), css(css(props.style)))); - assert(css([css(styles.base), css(css(props.style))])); - assert( - css( - css([css(styles.base), css(css(props.style))]), - css([css(styles.base), [css(css([css([[props.style]])]))]]), - ), - ); - }); - - it('deep merge ā {style} object', () => { - const assert = (res: t.CssTransformed) => { - expect(res.style).to.include({ color: 'red' }); - expect(res.style).to.include({ background: 'blue' }); - }; - - const props = { style: { color: 'red' } }; - const styles = { base: css({ background: 'blue' }) }; - - assert(css(styles.base, props.style)); - assert(css(styles.base, css(props.style))); - assert(css(css(styles.base), css(css(props.style)))); - assert(css([css(styles.base), css(css(props.style))])); - assert( - css( - css([css(styles.base), Style.css(css(props.style))]), - css([css(styles.base), [Style.css(css([css([[props.style]])]))]]), - ), - ); - }); - - it('cache name repeats (reused)', () => { - const props = { style: { color: 'red' } }; - const styles = { base: css({ background: 'blue' }) }; - - const a = css(css(styles.base), css(css(props.style))); - const b = css(css(styles.base), css(css(props.style))); - expect(a.hx).to.equal(b.hx); - }); - - it('wrapped ā equality', () => { - const a = css({ color: 'red' }); - const b = css(a); - const c = css([b]); - const d = css([a, c], b); - expect(b).to.eql(a); - expect(c).to.eql(a); - expect(d).to.eql(a); - }); - }); - }, -); diff --git a/code/sys.ui/ui-css/src/m.Style/common.ts b/code/sys.ui/ui-css/src/m.Style/common.ts index 5c551f465a..9b844d14bd 100644 --- a/code/sys.ui/ui-css/src/m.Style/common.ts +++ b/code/sys.ui/ui-css/src/m.Style/common.ts @@ -1,7 +1,7 @@ import toHash from 'hash-it'; import { DEFAULT as CssDomDefaults } from '../m.Css.Dom/common.ts'; -export { CssDom } from '../m.Css.Dom/mod.ts'; +export { CssDom, toString } from '../m.Css.Dom/mod.ts'; export { CssEdges } from '../m.Css.Edges/mod.ts'; export { CssTmpl } from '../m.Css.Tmpl/mod.ts'; export { toHash }; @@ -9,50 +9,13 @@ export { toHash }; export * from '../common.ts'; export const DEFAULT = { - get prefix() { - return CssDomDefaults.prefix; + get classPrefix() { + return CssDomDefaults.classPrefix; }, get pixelProps() { - return pixelProps; + return CssDomDefaults.pixelProps; }, get pseudoClasses() { return CssDomDefaults.pseudoClasses; }, } as const; - -/** - * CSS properties that accept unitless - * numbers (equating to "px" pixels). - */ -const pixelProps = new Set<string>([ - 'width', - 'height', - 'top', - 'right', - 'bottom', - 'left', - 'margin', - 'marginTop', - 'marginRight', - 'marginBottom', - 'marginLeft', - 'padding', - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - 'borderWidth', - 'borderTopWidth', - 'borderRightWidth', - 'borderBottomWidth', - 'borderLeftWidth', - 'borderRadius', - 'fontSize', - 'minWidth', - 'maxWidth', - 'minHeight', - 'maxHeight', - 'outlineWidth', - 'letterSpacing', - 'wordSpacing', -]); diff --git a/code/sys.ui/ui-css/src/m.Style/m.Style.ts b/code/sys.ui/ui-css/src/m.Style/m.Style.ts index 2ccab28192..ab27a0bf95 100644 --- a/code/sys.ui/ui-css/src/m.Style/m.Style.ts +++ b/code/sys.ui/ui-css/src/m.Style/m.Style.ts @@ -1,23 +1,21 @@ import { type t, Color } from './common.ts'; -import { DEFAULT, CssDom as Dom, CssEdges as Edges, CssTmpl as Tmpl } from './common.ts'; +import { CssDom as Dom, CssEdges as Edges, CssTmpl as Tmpl, toString } from './common.ts'; import { toShadow } from './u.toShadow.ts'; -import { toString } from './u.toString.ts'; import { transformer } from './u.transform.ts'; const { toPadding, toMargins } = Edges; -const prefix = DEFAULT.prefix; /** Perform a transformation on a loose set of CSS inputs. */ -export const css: t.CssTransform = transformer({ prefix }); +export const css: t.CssTransform = transformer({}); /** * CSS styling tools. */ export const Style: t.StyleLib = { + Dom, Color, Edges, - Dom, Tmpl, css, diff --git a/code/sys.ui/ui-css/src/m.Style/t.transform.ts b/code/sys.ui/ui-css/src/m.Style/t.transform.ts new file mode 100644 index 0000000000..3224849c8b --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Style/t.transform.ts @@ -0,0 +1,64 @@ +import type { t } from './common.ts'; + +/** Flags indicating the kind of string to export from the `toString` method. */ +export type CssTransformToStringKind = 'CssRule' | 'CssSelector'; + +/** + * Function that transforms 1..n CSS inputs into a style + * object that can be applied to a JSX element. + * + * NB: This is the raw transform containing the style along with cache metadata. + */ +export type CssTransform = (...input: t.CssInput[]) => t.CssTransformed; + +/** + * A transformed CSS properties object. + */ +export type CssTransformed = { + /** The hash of the style (used for caching). */ + readonly hx: number; + + /** Style properties. */ + readonly style: t.CssProps; + + /** The CSS class-name. */ + readonly class: t.CssClassname; + + /** Convert the {style} props object to a CSS string. */ + toString(kind?: t.CssTransformToStringKind): string; + + /** Retrieve the @container API scoped to the current css-class. */ + container(condition: string, style?: t.CssValue): t.CssTransformContainerBlock; + container(name: string, condition: string, style?: t.CssValue): t.CssTransformContainerBlock; + + /** Insert a CSS rule within scope with the current `class` name. */ + rule(selector: t.StringCssSelector, style: t.CssValue | t.CssValue[]): CssTransformed; +}; + +/** + * A specialised @container block API with concenience + * methods for the `CssTransform` functional object. + */ +export type CssTransformContainerBlock = { + /** The underlying @container block being used by the convenience API. */ + readonly block: t.CssDomContainerBlock; + + /** Insert a CSS rule within the @container context with the given arbitrary selector. */ + rule(selector: t.StringCssSelector, style: t.CssValue | t.CssValue[]): t.CssDomInsertedRule[]; + + /** Insert a CSS rule within the @container directly under the CSS class-name scope. */ + css(style: t.CssValue | t.CssValue[]): CssTransformContainerBlock; + + /** Creates a new scoped sub-selector. */ + nest(selector: t.StringCssSelector): CssTransformContainerBlock; + + /** + * Generate a new @container block off the root transform, + * used to create fluent chains of containers. + */ + container(condition: string, style?: t.CssValue): t.CssTransformContainerBlock; + container(name: string, condition: string, style?: t.CssValue): t.CssTransformContainerBlock; + + /** Returns the root `CssTransform` used in ending a fluent chain. */ + readonly done: CssTransformed; +}; diff --git a/code/sys.ui/ui-css/src/m.Style/t.ts b/code/sys.ui/ui-css/src/m.Style/t.ts index 0054784f30..d3dbf73e30 100644 --- a/code/sys.ui/ui-css/src/m.Style/t.ts +++ b/code/sys.ui/ui-css/src/m.Style/t.ts @@ -1,5 +1,6 @@ import type { CSSProperties } from 'react'; import type { t } from './common.ts'; +export type * from './t.transform.ts'; /** * CSS-Properties that accept string AND (inferable "unit" numbers) as values. @@ -10,15 +11,28 @@ import type { t } from './common.ts'; */ export type CssProps = CSSProperties; +/** + * Standard CSS properties with CSS-template extensions. + */ +export type CssValue = t.CssProps & t.CssPseudo & t.CssTemplates; +export type CssInput = + | t.CssValue + | undefined + | null + | false + | never + | t.CssTransformed + | CssInput[]; + /** * A CSS class-name. - * (no period, eg "foo" not ".foo") + * (no period, eg. "foo" not ".foo") */ export type CssClassname = string; export type CssClassPrefix = string; /** Options passed to `Style.transformer` factory function. */ -export type StyleTransformerOptions = { prefix?: string }; +export type StyleTransformerOptions = { sheet?: t.CssDomStylesheet }; /** * CSS styling tools. @@ -28,7 +42,7 @@ export type StyleLib = NamespaceLibs & { readonly css: t.CssTransform; /** Factory to produce `transform` function scoped to the given prefix. */ - transformer(options?: t.CssClassPrefix | t.StyleTransformerOptions): t.CssTransform; + transformer(options?: t.StyleTransformerOptions): t.CssTransform; /** Transform margin spacing. */ readonly toMargins: t.CssEdgesLib['toMargins']; @@ -55,40 +69,6 @@ type NamespaceLibs = { readonly Dom: t.CssDomLib; }; -/** - * Standard CSS properties with CSS-template extensions. - */ -export type CssValue = t.CssProps & t.CssPseudo & t.CssTemplates; -export type CssInput = t.CssValue | undefined | null | false | never | CssTransformed | CssInput[]; - -/** - * Function that transforms 1..n CSS inputs into a style - * object that can be applied to a JSX element. - * - * NB: This is the raw transform containing the style along with cache metadata. - */ -export type CssTransform = (...input: t.CssInput[]) => t.CssTransformed; - -/** - * A transformed CSS properties object. - */ -export type CssTransformed = { - /** The hash of the style (used for caching). */ - readonly hx: number; - - /** Style properties. */ - readonly style: t.CssProps; - - /** The CSS class-name. */ - readonly class: t.CssClassname; - - /** Convert the {style} props object to a CSS string. */ - toString(kind?: t.CssTransformStringKind): string; -}; - -/** Flags indicating the kind of string to export from the `toString` method. */ -export type CssTransformStringKind = 'CssRule' | 'CssSelector'; - /** * Shadow */ diff --git a/code/sys.ui/ui-css/src/m.Style/u.transform.container.ts b/code/sys.ui/ui-css/src/m.Style/u.transform.container.ts new file mode 100644 index 0000000000..92e3a21ab3 --- /dev/null +++ b/code/sys.ui/ui-css/src/m.Style/u.transform.container.ts @@ -0,0 +1,31 @@ +import { type t } from './common.ts'; + +/** + * Factory for creating the CSS @container convenience method + * for the `CssTransformer` API. + */ +export function createTransformContainer( + base: t.CssTransformed, + block: t.CssDomContainerBlock, + style?: t.CssProps | undefined, +): t.CssTransformContainerBlock { + const api: t.CssTransformContainerBlock = { + block, + + container: base.container, + rule: (selector, style) => block.rules.add(selector, style), + nest: (selector) => createTransformContainer(base, block.scope(selector)), + + css(style) { + block.rules.add('', style); + return api; + }, + + get done() { + return base; + }, + }; + + if (style) api.css(style); + return api; +} diff --git a/code/sys.ui/ui-css/src/m.Style/u.transform.ts b/code/sys.ui/ui-css/src/m.Style/u.transform.ts index 7856140f52..c7ff8e7cb7 100644 --- a/code/sys.ui/ui-css/src/m.Style/u.transform.ts +++ b/code/sys.ui/ui-css/src/m.Style/u.transform.ts @@ -1,6 +1,6 @@ -import { type t, CssDom, CssTmpl, DEFAULT, toHash } from './common.ts'; +import { type t, CssDom, CssTmpl, toHash, toString } from './common.ts'; import { isTransformed } from './u.is.ts'; -import { toString } from './u.toString.ts'; +import { createTransformContainer } from './u.transform.container.ts'; type M = Map<number, t.CssTransformed>; type O = Record<string, unknown>; @@ -9,20 +9,29 @@ type F = t.StyleLib['transformer']; /** * Generator (factory). */ -export const transformer: F = (input) => { - const options = wrangle.options(input); - const { prefix = DEFAULT.prefix } = options; - const dom = CssDom.create(prefix); +export const transformer: F = (options = {}) => { const cache = new Map<number, t.CssTransformed>(); - const fn: t.CssTransform = (...input) => transform({ dom, cache, input }); + + let _sheet: t.CssDomStylesheet | undefined; + const lazySheet = () => options.sheet ?? _sheet ?? CssDom.stylesheet(/* default config */); + + const fn: t.CssTransform = (...input) => { + const sheet = lazySheet(); + return transform({ sheet, cache, input }); + }; return fn; }; /** * Perform a cacheable transformation on a loose set of CSS inputs. */ -function transform(args: { dom: t.CssDom; cache: M; input: t.CssInput[] }): t.CssTransformed { - const { dom, cache } = args; +function transform(args: { + sheet: t.CssDomStylesheet; + cache: M; + input: t.CssInput[]; +}): t.CssTransformed { + const { sheet, cache } = args; + const style: t.CssProps = CssTmpl.transform(wrangle.input(args.input)); const hx = toHash(style); if (cache.has(hx)) return cache.get(hx)!; @@ -33,7 +42,8 @@ function transform(args: { dom: t.CssDom; cache: M; input: t.CssInput[] }): t.Cs return style; }, get class() { - return dom.class(style, hx); + const classes = sheet.classes(); + return classes.add(style, { hx }); }, toString(kind = 'CssRule') { const rule = toString(style); @@ -41,6 +51,16 @@ function transform(args: { dom: t.CssDom; cache: M; input: t.CssInput[] }): t.Cs if (kind === 'CssSelector') return `.${api.class} { ${rule} }`; throw new Error(`Kind '${kind}' not supported`); }, + container(...args: any[]) { + const { name, condition, style } = wrangle.containerArgs(args); + const container = name ? sheet.container(name, condition) : sheet.container(condition); + const block = container.scope(`.${api.class}`); + return createTransformContainer(api, block, style); + }, + rule(selector, style) { + sheet.rules.add(`.${api.class} ${selector}`.trim(), style); + return api; + }, }; cache.set(hx, api); @@ -48,15 +68,9 @@ function transform(args: { dom: t.CssDom; cache: M; input: t.CssInput[] }): t.Cs } /** - * Helpers + * Helpers: */ const wrangle = { - options(input?: Parameters<F>[0]): t.StyleTransformerOptions { - if (!input) return {}; - if (typeof input === 'string') return { prefix: input }; - return input; - }, - input(input: any): t.CssProps { if (Array.isArray(input)) { return input.reduce((acc, next) => ({ ...acc, ...wrangle.input(next) }), {} as O); @@ -66,4 +80,24 @@ const wrangle = { return input; } }, + + containerArgs(args: any[]) { + const done = (condition: string, name?: string, style?: t.CssProps) => { + name = name ? name.trim() : name; + condition = condition ? condition.trim() : ''; + return { name, condition, style }; + }; + if (!args || args.length === 0) return done(''); + if (args.length === 1) return done(args[0]); + if (args.length === 2) { + const [p1, p2] = args; + if (typeof p2 === 'object') return done(p1, undefined, p2); + if (typeof p2 === 'string') return done(p2, p1); + } + if (args.length === 3) { + const [p1, p2, p3] = args; + return done(p2, p1, p3); + } + throw new Error(`Failed to parse [container.scope] arguments: ${args}`); + }, } as const; diff --git a/code/sys.ui/ui-css/src/pkg.ts b/code/sys.ui/ui-css/src/pkg.ts index 79cb3f9c80..6ee002d709 100644 --- a/code/sys.ui/ui-css/src/pkg.ts +++ b/code/sys.ui/ui-css/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/ui-css', version: '0.0.81' }; diff --git a/code/sys.ui/ui-css/src/types.ts b/code/sys.ui/ui-css/src/types.ts index f1205036d0..585df0d03d 100644 --- a/code/sys.ui/ui-css/src/types.ts +++ b/code/sys.ui/ui-css/src/types.ts @@ -6,3 +6,6 @@ export type * from './m.Css.Dom/t.ts'; export type * from './m.Css.Edges/t.ts'; export type * from './m.Css.Tmpl/t.ts'; export type * from './m.Style/t.ts'; + +/** A string that represents a CSS selector. */ +export type StringCssSelector = string; diff --git a/code/sys.ui/ui-dom/deno.json b/code/sys.ui/ui-dom/deno.json index da5555f21d..3dfa098468 100644 --- a/code/sys.ui/ui-dom/deno.json +++ b/code/sys.ui/ui-dom/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/ui-dom", - "version": "0.0.77", + "version": "0.0.87", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys.ui/ui-dom/src/pkg.ts b/code/sys.ui/ui-dom/src/pkg.ts index 79cb3f9c80..e449bfe858 100644 --- a/code/sys.ui/ui-dom/src/pkg.ts +++ b/code/sys.ui/ui-dom/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/ui-dom', version: '0.0.87' }; diff --git a/code/sys.ui/ui-react-components/-scripts/-clean.ts b/code/sys.ui/ui-react-components/-scripts/-clean.ts deleted file mode 100644 index 30ae5e6470..0000000000 --- a/code/sys.ui/ui-react-components/-scripts/-clean.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Fs } from '@sys/fs'; - -const removeDir = (path: string) => Fs.remove(Fs.resolve(path), { log: true }); -await removeDir('./.tmp'); diff --git a/code/sys.ui/ui-react-components/-scripts/-main.ts b/code/sys.ui/ui-react-components/-scripts/-main.ts index b0fd86ea46..16e17b93a6 100644 --- a/code/sys.ui/ui-react-components/-scripts/-main.ts +++ b/code/sys.ui/ui-react-components/-scripts/-main.ts @@ -1,3 +1 @@ -import { ViteEntry } from '@sys/driver-vite'; -await ViteEntry.main(Deno.args); -Deno.exit(0); +import '../../../sys.driver/driver-vite/src/-entry/-main.ts'; diff --git a/code/sys.ui/ui-react-components/-scripts/-tmp.ts b/code/sys.ui/ui-react-components/-scripts/-tmp.ts new file mode 100644 index 0000000000..ba662b31d4 --- /dev/null +++ b/code/sys.ui/ui-react-components/-scripts/-tmp.ts @@ -0,0 +1 @@ +console.info(`\nš ${import.meta.url}\n`); diff --git a/code/sys.ui/ui-react-components/README.md b/code/sys.ui/ui-react-components/README.md index b91c224a0b..c2b9509266 100644 --- a/code/sys.ui/ui-react-components/README.md +++ b/code/sys.ui/ui-react-components/README.md @@ -1,3 +1,2 @@ -# UI Components (Common) - - +# UI Components +Library of common `<React>` system components. diff --git a/code/sys.ui/ui-react-components/deno.json b/code/sys.ui/ui-react-components/deno.json index a2bcb0d348..5020c99b6b 100644 --- a/code/sys.ui/ui-react-components/deno.json +++ b/code/sys.ui/ui-react-components/deno.json @@ -1,19 +1,23 @@ { "name": "@sys/ui-react-components", - "version": "0.0.38", - "license": "MIT", + "version": "0.0.41", "tasks": { - "lint": "deno lint", - "dry": "deno publish --allow-dirty --dry-run", - "clean": "deno run -RWE ./-scripts/-clean.ts", - "test": "deno test -RWNE --allow-run --allow-ffi", - "dev": "deno run -A ./-scripts/-main.ts --cmd=dev --in=./src/-test/index.html", - "build": "deno run -A ./-scripts/-main.ts --cmd=build --in=./src/-test/index.html", - "serve": "deno run -A ./-scripts/-main.ts --cmd=serve --in=./src/-test/index.html" + "dev": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=dev --in=./src/-test/index.html", + "build": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=build --in=./src/-test/index.html", + "serve": "deno run -RNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=serve", + "test": "deno test -RWNE --allow-run --allow-ffi --allow-sys", + "dry": "deno publish --allow-dirty --dry-run", + "clean": "deno run -RWE --allow-ffi ./-scripts/-main.ts --cmd=clean", + "upgrade": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=upgrade", + "backup": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=backup", + "help": "deno run -RE --allow-ffi ./-scripts/-main.ts --cmd=help", + "tmp": "deno run -A ./-scripts/-tmp.ts" }, "exports": { ".": "./src/mod.ts", "./t": "./src/types.ts", - "./types": "./src/types.ts" - } + "./types": "./src/types.ts", + "./specs": "./src/-test/entry.Specs.ts" + }, + "license": "MIT" } diff --git a/code/sys.ui/ui-react-components/src/-sample/-css-container/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/-sample/-css-container/-SPEC.Debug.tsx new file mode 100644 index 0000000000..31123c5cb2 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-css-container/-SPEC.Debug.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { type t, Button, Color, css, Signal } from '../-test.ui.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { theme: s<t.CommonTheme>('Light') }; + const api = { props }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<P> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => p.theme.value); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ + // color: theme.fg, + }), + title: css({ fontWeight: 'bold' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.title.class}>CSS: @container</div> + <hr /> + + <Button + block + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/-sample/-css-container/-SPEC.tsx b/code/sys.ui/ui-react-components/src/-sample/-css-container/-SPEC.tsx new file mode 100644 index 0000000000..79cc8c437f --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-css-container/-SPEC.tsx @@ -0,0 +1,28 @@ +import { Dev, Spec, Signal } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Container } from './ui.tsx'; + +export default Spec.describe('css:container-type', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + // š· TODO: hook into signals here. + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => <Container theme={p.theme.value} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/-sample/-css-container/styles.css b/code/sys.ui/ui-react-components/src/-sample/-css-container/styles.css new file mode 100644 index 0000000000..c456e785d1 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-css-container/styles.css @@ -0,0 +1,20 @@ +/* + SAMPLE: + See the equivalent programmatic [css-in-js] in "./ui.tsx". + + @container ā MDN Docs: + https://developer.mozilla.org/en-US/docs/Web/CSS/@container + + Baseline compatible "broadly supported across the major browsers as of <2023>" + https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility +*/ + +.card h2 { + font-size: 2em; +} + +@container (min-width: 700px) { + .card h2 { + font-size: 5em; + } +} diff --git a/code/sys.ui/ui-react-components/src/-sample/-css-container/ui.tsx b/code/sys.ui/ui-react-components/src/-sample/-css-container/ui.tsx new file mode 100644 index 0000000000..d838841fcc --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-css-container/ui.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { type t, Color, css, Style, useSizeObserver } from '../-test.ui.ts'; + +/** + * š¼ NOTE: import the raw stylesheet + * + * @container ā MDN Docs: + * https://developer.mozilla.org/en-US/docs/Web/CSS/@container + * Compatibility: + * Baseline compatible ā "broadly supported across the major browsers as of <2023>" + * https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility + * + */ +// import './styles.css'; + +export type ContainerProps = { + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = ContainerProps; + +/** + * Component: + */ +export const Container: React.FC<P> = (props) => { + const {} = props; + + const size = useSizeObserver(); + + /** + * REF: https://chatgpt.com/share/67ddffa7-a668-800b-87f1-4aa855733c7b + */ + React.useEffect(() => { + // return; + const sheet = Style.Dom.stylesheet(); + + // sheet.rule('.card h2', { fontSize: 50 }); + // sheet.rule('.card h2', { fontSize: 100 }, { context: '@container (min-width: 600px)' }); + + // const container = sheet.container('min-width: 600px'); + // container.rules.add('.card h2', { fontSize: 150 }); + + // sheet.rule(`.${styles.base.class} h2`, { fontSize: 50 }); + // const container = sheet.container('min-width: 600px'); + // container.rules.add(`.${styles.base.class} h2`, { fontSize: 150 }); + + // const container = sheet.container('min-width: 600px'); + // const scope = container.scope(`.${styles.h2.class}`); + + // const scope = styles.h2.container('min-width: 600px'); + // scope.rules.add('', { fontSize: 100 }); + }, []); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const condition = 'min-width: 600px'; + + const styles = { + base: css({ + padding: 30, + color: theme.fg, + letterSpacing: -0.5, + containerType: 'size', // š¼ ā NB: turn this on for the @container rules to take effect (width AND height). + // containerType: 'inline-size', // š¼ ā NB: turn this on for the @container rules to take effect (width only). + overflow: 'hidden', + display: 'grid', + placeItems: 'center', + + // color: 'orange', + }) + .rule('h2', { color: 'red' }) + .rule('h2 code', { color: 'blue' }), // š¼ NB: aribitrary CSS sub-selector rule. + + h1: css({ + color: 'red', + fontSize: 50, + transition: 'font-size 200ms, color 200ms', + whiteSpace: 'nowrap', + }) + .container('min-width: 400px', { fontSize: 90, color: 'blue' }) + .container('min-width: 600px', { fontSize: 150, color: theme.fg, letterSpacing: -4 }) + .container('max-height: 500px', { color: 'orange' }).done, + + size: css({ Absolute: [8, 10, null, null], fontSize: 16 }), + }; + + // styles.base. + + /** + * Equivalent: (sample code) + */ + // const container = styles.base.container('min-width: 600px'); + // container.rule('h2', { + // fontSize: 140, + // color: 'blue', + // letterSpacing: -3, + // transition: 'font-size 200ms', + // }); + + const elSize = <div className={styles.size.class}>{`${size.toString()}`}</div>; + + return ( + <div ref={size.ref} className={css(styles.base, props.style).class}> + <h1 className={styles.h1.class}>{`Hello š`}</h1> + <h2> + {`H2 Heading`} <code>Code</code> + </h2> + {elSize} + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/-SPEC.Debug.tsx new file mode 100644 index 0000000000..2763b7dd7d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/-SPEC.Debug.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { type t, Button, Color, css, Signal } from '../-test.ui.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = {}; + const api = { + props, + listen() { + const p = props; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'useMouse'}</div> + <div /> + <div>{'Hook'}</div> + </div> + + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/-SPEC.tsx b/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/-SPEC.tsx new file mode 100644 index 0000000000..d189ad0954 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/-SPEC.tsx @@ -0,0 +1,26 @@ +import { Dev, Spec, Signal } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Sample } from './ui.tsx'; + +export default Spec.describe('MyComponent', (e) => { + const debug = createDebugSignals(); + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size() + .display('grid') + .render((e) => <Sample debug={debug} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/ui.tsx b/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/ui.tsx new file mode 100644 index 0000000000..cd8ba4b16c --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-dom-useMouse/ui.tsx @@ -0,0 +1,39 @@ +import { useClickInside, useClickOutside, useMouse } from '@sys/ui-react'; +import React from 'react'; + +import { type t, css } from '../-test.ui.ts'; +import { type DebugSignals } from './-SPEC.Debug.tsx'; + +export type SampleProps = { + debug: DebugSignals; + style?: t.CssInput; +}; + +export const Sample: React.FC<SampleProps> = (props) => { + const { debug } = props; + + const ref = React.useRef<HTMLDivElement>(null); + useClickInside({ ref, callback: (e) => console.info(`ā”ļø click-inside:`, e) }); + useClickOutside({ ref, callback: (e) => console.info(`ā”ļø click-outside:`, e) }); + + const mouse = useMouse(); + console.info('mouse.is', mouse.is); + + /** + * Render: + */ + const styles = { + base: css({ + position: 'relative', + overflow: 'hidden', + userSelect: 'none', + padding: 20, + }), + }; + + return ( + <div ref={ref} className={css(styles.base, props.style).class}> + <div {...mouse.handlers}>{'š· Hello'}</div> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/-SPEC.Debug.tsx new file mode 100644 index 0000000000..a96ba679db --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/-SPEC.Debug.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { type t, css, ObjectView, Signal } from '../-test.ui.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const api = { rect: s<t.DomRect>() }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const rect = debug.rect.value; + + Signal.useRedrawEffect(() => debug.rect.value); + + /** + * Render: + */ + const styles = { + base: css({}), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div>ā”ļø via signal:</div> + <ObjectView name={'rect'} data={rect} margin={[5, 22]} expand={1} /> + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/-SPEC.tsx b/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/-SPEC.tsx new file mode 100644 index 0000000000..e27bbb8ba6 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/-SPEC.tsx @@ -0,0 +1,26 @@ +import { Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Sample } from './ui.tsx'; + +export default Spec.describe('useSizeObserver', (e) => { + const debug = createDebugSignals(); + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Signal.effect(() => { + debug.rect.value; + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => <Sample debug={debug} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/ui.tsx b/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/ui.tsx new file mode 100644 index 0000000000..d86c091665 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-dom-useSizeObserver/ui.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { type t, css, ObjectView, useSizeObserver } from '../-test.ui.ts'; +import { type DebugSignals } from './-SPEC.Debug.tsx'; + +export type SampleProps = { + debug: DebugSignals; + style?: t.CssInput; +}; + +/** + * Component: + */ +export const Sample: React.FC<SampleProps> = (props) => { + const { debug } = props; + const size = useSizeObserver((e) => (debug.rect.value = e.toObject())); + + /** + * Render: + */ + const styles = { + base: css({ position: 'relative', overflow: 'hidden' }), + body: css({ boxSizing: 'border-box', margin: 20 }), + }; + + return ( + <div ref={size.ref} className={css(styles.base, props.style).class}> + <div className={styles.body.class}> + <div>{'š useSizeObserver:'}</div> + <ObjectView name={'size.rect'} data={size.toObject()} block margin={[10, 20]} /> + </div> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/-sample/-test.ui.ts b/code/sys.ui/ui-react-components/src/-sample/-test.ui.ts new file mode 100644 index 0000000000..6077d9257d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/-test.ui.ts @@ -0,0 +1,3 @@ +export * from '../ui/-test.ui.ts'; +export { Button } from '../ui/Button/mod.ts'; +export { ObjectView } from '../ui/ObjectView/mod.ts'; diff --git a/code/sys.ui/ui-react-components/src/-sample/images/sample.larger.svg b/code/sys.ui/ui-react-components/src/-sample/images/sample.larger.svg new file mode 100644 index 0000000000..67dde8d42e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/images/sample.larger.svg @@ -0,0 +1,24 @@ +<svg width="233" height="98" viewBox="0 0 233 98" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_711_130)"> +<rect width="233" height="98" fill="white"/> +<path d="M85.82 70.186C85.572 70.186 85.3447 70.1033 85.138 69.938C84.9727 69.7727 84.89 69.5867 84.89 69.38C84.89 68.9253 85.2207 68.6153 85.882 68.45C87.2873 68.0367 88.4653 67.6853 89.416 67.396C90.3667 67.0653 91.0693 66.6107 91.524 66.032C92.02 65.412 92.268 64.544 92.268 63.428V40.178C92.268 39.3513 92.02 38.7107 91.524 38.256C91.028 37.8013 90.3667 37.4913 89.54 37.326C88.7547 37.1193 87.9073 36.9747 86.998 36.892C86.626 36.8507 86.254 36.7473 85.882 36.582C85.5513 36.3753 85.386 36.086 85.386 35.714C85.386 35.342 85.5513 35.0527 85.882 34.846C86.2127 34.598 86.6053 34.4533 87.06 34.412C89.1267 34.164 90.966 33.8127 92.578 33.358C94.2313 32.862 95.864 31.9527 97.476 30.63C97.5173 30.5887 97.5793 30.568 97.662 30.568C97.7447 30.5267 97.8273 30.506 97.91 30.506C98.158 30.506 98.3647 30.568 98.53 30.692C98.7367 30.816 98.84 30.9607 98.84 31.126C98.7573 31.7047 98.6747 32.3453 98.592 33.048C98.5507 33.7507 98.53 34.5773 98.53 35.528V63.986C98.53 64.9367 98.7367 65.6807 99.15 66.218C99.6047 66.714 100.287 67.1273 101.196 67.458C102.147 67.7473 103.387 68.078 104.916 68.45C105.205 68.5327 105.453 68.6567 105.66 68.822C105.867 68.946 105.97 69.1527 105.97 69.442C105.97 69.938 105.681 70.186 105.102 70.186C104.027 70.186 102.932 70.1447 101.816 70.062C100.741 70.0207 99.6667 69.9793 98.592 69.938C97.5587 69.8967 96.5253 69.876 95.492 69.876C94.4587 69.876 93.4047 69.8967 92.33 69.938C91.2553 69.9793 90.16 70.0207 89.044 70.062C87.9693 70.1447 86.8947 70.186 85.82 70.186ZM126 70.93C123.603 70.93 121.433 70.434 119.49 69.442C117.548 68.45 115.998 67.0033 114.84 65.102C113.683 63.1593 113.104 60.8033 113.104 58.034C113.104 55.7193 113.373 53.632 113.91 51.772C114.448 49.8707 115.109 48.1967 115.894 46.75C116.721 45.262 117.548 44.0013 118.374 42.968C119.201 41.9347 119.904 41.108 120.482 40.488C121.474 39.4133 122.756 38.2353 124.326 36.954C125.938 35.6727 127.798 34.4533 129.906 33.296C132.014 32.0973 134.288 31.1467 136.726 30.444C137.181 30.2787 137.594 30.2373 137.966 30.32C138.338 30.4027 138.483 30.5473 138.4 30.754C138.318 31.126 138.09 31.436 137.718 31.684C137.346 31.8907 136.974 32.0767 136.602 32.242C133.998 33.358 131.766 34.66 129.906 36.148C128.088 37.636 126.434 39.4753 124.946 41.666C124.326 42.658 123.582 44.0013 122.714 45.696C121.846 47.3907 121.102 49.292 120.482 51.4C119.862 53.508 119.552 55.6573 119.552 57.848C119.594 60.1213 120.007 62.0433 120.792 63.614C121.578 65.1847 122.57 66.3833 123.768 67.21C124.967 68.0367 126.166 68.45 127.364 68.45C128.894 68.4913 130.216 67.892 131.332 66.652C132.49 65.412 133.068 63.5313 133.068 61.01C133.068 59.7287 132.841 58.5713 132.386 57.538C131.932 56.4633 131.312 55.5953 130.526 54.934C129.741 54.2313 128.852 53.88 127.86 53.88C126.992 53.88 126.228 53.942 125.566 54.066C124.946 54.1487 124.347 54.3347 123.768 54.624C123.272 54.872 122.962 54.872 122.838 54.624C122.756 54.3347 122.859 54.004 123.148 53.632C123.438 53.3427 123.686 53.0947 123.892 52.888C124.14 52.6813 124.43 52.4747 124.76 52.268C125.256 51.8547 126.124 51.3793 127.364 50.842C128.604 50.2633 129.782 49.974 130.898 49.974C132.386 49.974 133.75 50.3253 134.99 51.028C136.272 51.7307 137.305 52.6813 138.09 53.88C138.876 55.0787 139.268 56.4633 139.268 58.034C139.268 59.8527 138.896 61.5473 138.152 63.118C137.45 64.6887 136.478 66.0733 135.238 67.272C133.998 68.4293 132.572 69.3387 130.96 70C129.39 70.62 127.736 70.93 126 70.93Z" fill="#383057" fill-opacity="0.7"/> +<path d="M143.117 70.186C142.207 70.186 141.753 69.938 141.753 69.442C141.753 69.1113 141.897 68.884 142.187 68.76C142.476 68.636 142.807 68.4913 143.179 68.326C143.799 68.1193 144.377 67.8713 144.915 67.582C145.452 67.2927 145.721 66.714 145.721 65.846V35.28C145.721 33.916 145.535 32.8827 145.163 32.18C144.791 31.4773 144.088 31.002 143.055 30.754C142.683 30.6713 142.497 30.382 142.497 29.886C142.497 29.4313 142.662 29.1627 142.993 29.08C144.067 28.8733 145.059 28.6047 145.969 28.274C146.919 27.9433 147.787 27.6333 148.573 27.344C149.399 27.0133 150.123 26.724 150.743 26.476C151.032 26.352 151.259 26.29 151.425 26.29C151.59 26.29 151.693 26.352 151.735 26.476C151.817 26.5587 151.859 26.7033 151.859 26.91C151.859 27.282 151.776 28.0053 151.611 29.08C151.487 30.1547 151.425 31.7253 151.425 33.792V55.12C151.425 55.5333 151.549 55.74 151.797 55.74C152.003 55.74 152.272 55.6573 152.603 55.492C152.933 55.3267 153.264 55.0993 153.595 54.81C155.248 53.4047 156.653 52.082 157.811 50.842C158.968 49.602 159.753 48.7133 160.167 48.176C160.332 47.928 160.456 47.742 160.539 47.618C160.663 47.4527 160.725 47.308 160.725 47.184C160.725 46.9773 160.539 46.7913 160.167 46.626C159.836 46.4193 159.443 46.2747 158.989 46.192C158.493 46.1093 158.245 45.9027 158.245 45.572C158.245 45.324 158.369 45.138 158.617 45.014C158.906 44.8487 159.237 44.766 159.609 44.766H162.647C163.308 44.766 164.073 44.7247 164.941 44.642C165.85 44.518 166.718 44.394 167.545 44.27C168.371 44.146 168.991 44.084 169.405 44.084C169.942 44.084 170.211 44.3113 170.211 44.766C170.211 45.014 170.066 45.2413 169.777 45.448C169.529 45.6547 169.177 45.82 168.723 45.944C167.979 46.1093 167.214 46.3987 166.429 46.812C165.643 47.184 164.899 47.5767 164.197 47.99C163.783 48.238 163.143 48.6927 162.275 49.354C161.407 50.0153 160.477 50.7593 159.485 51.586C158.493 52.4127 157.563 53.2187 156.695 54.004C156.447 54.2107 156.323 54.4173 156.323 54.624C156.323 54.7067 156.343 54.7893 156.385 54.872C156.467 54.9547 156.529 55.0373 156.571 55.12C157.645 56.4427 158.741 57.7653 159.857 59.088C160.973 60.4107 162.027 61.6093 163.019 62.684C164.011 63.7587 164.817 64.6267 165.437 65.288C166.139 65.9907 166.945 66.6107 167.855 67.148C168.805 67.6853 169.591 68.078 170.211 68.326C170.624 68.45 171.017 68.5947 171.389 68.76C171.802 68.884 172.009 69.1113 172.009 69.442C172.009 69.938 171.595 70.186 170.769 70.186C168.826 70.186 166.78 70.1447 164.631 70.062C162.523 70.0207 160.415 70 158.307 70C157.521 70 157.129 69.7727 157.129 69.318C157.129 69.07 157.232 68.8633 157.439 68.698C157.687 68.4913 157.976 68.3467 158.307 68.264C158.72 68.0987 159.051 67.9333 159.299 67.768C159.588 67.5613 159.733 67.2927 159.733 66.962C159.733 66.838 159.691 66.6933 159.609 66.528C159.526 66.3627 159.402 66.1767 159.237 65.97L152.231 57.538C152.065 57.29 151.9 57.166 151.735 57.166C151.528 57.2073 151.425 57.352 151.425 57.6C151.383 58.22 151.363 58.8607 151.363 59.522C151.363 60.1833 151.363 60.8653 151.363 61.568C151.363 62.2293 151.363 62.9527 151.363 63.738C151.363 64.482 151.383 65.2053 151.425 65.908C151.425 66.652 151.611 67.1893 151.983 67.52C152.355 67.8507 152.851 68.1193 153.471 68.326C153.801 68.45 154.111 68.5947 154.401 68.76C154.69 68.884 154.835 69.1113 154.835 69.442C154.835 69.7313 154.669 69.9173 154.339 70C154.008 70.124 153.739 70.186 153.533 70.186C152.747 70.186 152.107 70.1447 151.611 70.062C151.115 70.0207 150.66 69.9793 150.247 69.938C149.833 69.8967 149.255 69.876 148.511 69.876C147.808 69.876 147.209 69.8967 146.713 69.938C146.217 69.9793 145.7 70.0207 145.163 70.062C144.625 70.1447 143.943 70.186 143.117 70.186ZM187.825 70.868C186.791 70.868 185.799 70.7647 184.849 70.558C183.898 70.3927 183.009 70.2067 182.183 70C181.397 69.7933 180.695 69.6073 180.075 69.442C179.455 69.2353 179.021 69.132 178.773 69.132C178.442 69.132 178.132 69.2147 177.843 69.38C177.595 69.5453 177.347 69.7313 177.099 69.938C176.809 70.186 176.582 70.31 176.417 70.31C176.169 70.31 175.941 70.2273 175.735 70.062C175.528 69.8967 175.425 69.7107 175.425 69.504C175.425 69.132 175.507 68.4707 175.673 67.52C175.879 66.5693 175.983 65.5567 175.983 64.482L175.859 35.28C175.859 34.2053 175.631 33.2753 175.177 32.49C174.722 31.6633 174.061 31.0847 173.193 30.754C172.986 30.6713 172.8 30.568 172.635 30.444C172.469 30.2787 172.387 30.072 172.387 29.824C172.387 29.6173 172.449 29.452 172.573 29.328C172.738 29.204 172.924 29.1213 173.131 29.08C173.792 28.956 174.639 28.7287 175.673 28.398C176.706 28.026 177.677 27.654 178.587 27.282C179.537 26.91 180.219 26.6413 180.633 26.476C180.922 26.352 181.129 26.29 181.253 26.29C181.418 26.29 181.542 26.352 181.625 26.476C181.707 26.5587 181.749 26.6827 181.749 26.848C181.749 27.0547 181.707 27.6953 181.625 28.77C181.583 29.8033 181.563 31.0227 181.563 32.428L181.501 45.324C181.501 46.0267 181.542 46.5227 181.625 46.812C181.749 47.1013 181.893 47.246 182.059 47.246C183.34 46.254 184.766 45.4687 186.337 44.89C187.949 44.27 189.416 43.96 190.739 43.96C192.847 43.96 194.748 44.4973 196.443 45.572C198.137 46.6467 199.481 48.1347 200.473 50.036C201.506 51.896 202.023 54.004 202.023 56.36C202.023 59.212 201.423 61.7333 200.225 63.924C199.026 66.1147 197.352 67.83 195.203 69.07C193.095 70.2687 190.635 70.868 187.825 70.868ZM189.127 68.698C190.532 68.698 191.731 68.2227 192.723 67.272C193.756 66.28 194.541 64.9573 195.079 63.304C195.657 61.6507 195.947 59.832 195.947 57.848C195.947 56.1947 195.678 54.5207 195.141 52.826C194.603 51.1313 193.756 49.726 192.599 48.61C191.441 47.4527 189.974 46.874 188.197 46.874C186.874 46.874 185.634 47.0807 184.477 47.494C183.319 47.9073 182.327 48.4653 181.501 49.168V61.63C181.501 63.0353 181.852 64.2753 182.555 65.35C183.299 66.4247 184.249 67.2513 185.407 67.83C186.605 68.4087 187.845 68.698 189.127 68.698Z" fill="#F60000"/> +<path d="M209.953 71.24C208.878 71.24 207.948 70.8473 207.163 70.062C206.377 69.2767 205.985 68.3467 205.985 67.272C205.985 66.1147 206.377 65.164 207.163 64.42C207.948 63.6347 208.878 63.242 209.953 63.242C211.069 63.242 211.999 63.6347 212.743 64.42C213.528 65.164 213.921 66.1147 213.921 67.272C213.921 68.3467 213.528 69.2767 212.743 70.062C211.999 70.8473 211.069 71.24 209.953 71.24Z" fill="#383057"/> +<mask id="mask0_711_130" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="14" y="11" width="57" height="90"> +<path d="M14 11H70.124V100.383H14V11Z" fill="white"/> +</mask> +<g mask="url(#mask0_711_130)"> +<mask id="mask1_711_130" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="14" y="11" width="50" height="79"> +<path d="M14 11H63.116V89.221H14V11Z" fill="white"/> +</mask> +<g mask="url(#mask1_711_130)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M60.156 23.301C61.7828 24.93 62.7 27.1358 62.708 29.438L62.835 80.21C62.8312 82.5988 61.882 84.8889 60.195 86.58C58.5107 88.2682 56.2247 89.2182 53.84 89.221H49.064C48.536 89.221 48.03 89.011 47.657 88.637C47.2833 88.2625 47.0733 87.7551 47.073 87.226V75.181H22.996C21.8151 75.18 20.6459 74.9463 19.5554 74.4933C18.4648 74.0404 17.4741 73.3769 16.64 72.541C14.9527 70.8496 14.0036 68.5591 14 66.17V19.7C14.0016 17.3943 14.9163 15.183 16.544 13.55C17.3487 12.743 18.3045 12.1026 19.3568 11.6652C20.4092 11.2278 21.5374 11.0021 22.677 11.001H27.74C28.1382 10.9944 28.5291 11.1076 28.8621 11.3261C29.195 11.5445 29.4546 11.8581 29.607 12.226C29.71 12.47 29.762 12.732 29.762 12.996V20.755H54.031C56.328 20.759 58.531 21.675 60.156 23.302V23.301ZM19.358 16.373C18.4772 17.2568 17.9818 18.4532 17.98 19.701L17.988 58.691C19.4662 57.6911 21.2104 57.1575 22.995 57.159H25.782V14.992H22.677C21.4313 14.9941 20.2375 15.4909 19.358 16.373ZM22.996 71.189C22.3375 71.189 21.6856 71.0591 21.0774 70.8067C20.4692 70.5543 19.9169 70.1843 19.452 69.718C18.5112 68.7749 17.9829 67.4971 17.9829 66.165C17.9829 64.8329 18.5112 63.5551 19.452 62.612C19.9169 62.1457 20.4692 61.7757 21.0774 61.5233C21.6856 61.2709 22.3375 61.141 22.996 61.141H27.772C28.3 61.141 28.806 60.931 29.179 60.556C29.552 60.182 29.762 59.674 29.762 59.146V24.736H54.031C55.2753 24.7384 56.4679 25.2343 57.347 26.115C58.2272 26.997 58.7232 28.191 58.727 29.437L58.831 72.706C57.3564 71.7119 55.6184 71.1812 53.84 71.182L22.996 71.19V71.189ZM51.054 85.23H53.84C55.169 85.23 56.444 84.701 57.383 83.759C58.324 82.816 58.8525 81.5382 58.8525 80.206C58.8525 78.8738 58.324 77.596 57.383 76.653C56.9182 76.1869 56.366 75.817 55.758 75.5646C55.1501 75.3122 54.4983 75.1822 53.84 75.182H51.054V85.23Z" fill="#F60000"/> +</g> +</g> +</g> +<defs> +<clipPath id="clip0_711_130"> +<rect width="233" height="98" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/code/sys.ui/ui-react-components/src/-sample/images/sample.small.svg b/code/sys.ui/ui-react-components/src/-sample/images/sample.small.svg new file mode 100644 index 0000000000..614640ea09 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-sample/images/sample.small.svg @@ -0,0 +1,22 @@ +<svg width="1059" height="1059" viewBox="0 0 1059 1059" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="svg.sample" clip-path="url(#clip0)"> +<rect id="border-outline" x="114.792" y="114.792" width="828.646" height="828.646" rx="149.98" stroke="#383057" stroke-width="26.1768"/> +<g id="inner"> +<g id="tick"> +<path id="Vector" d="M748.501 529.114L699.837 473.469L706.618 399.875L634.62 383.521L596.925 319.699L529.115 348.818L461.305 319.699L423.61 383.321L351.611 399.476L358.392 473.27L309.729 529.114L358.392 584.758L351.611 658.552L423.61 674.906L461.305 738.528L529.115 709.21L596.925 738.328L634.62 674.706L706.618 658.352L699.837 584.758L748.501 529.114ZM491.021 623.25L415.233 547.263L444.751 517.745L491.021 564.215L607.695 447.143L637.213 476.66L491.021 623.25Z" fill="#383057"/> +</g> +<circle id="circle" cx="529.114" cy="529.114" r="313.049" stroke="#383057" stroke-width="2.14564" stroke-dasharray="12.87 12.87"/> +</g> +<g id="corner-pointers" opacity="0.5"> +<line id="Line 3125" x1="0.353553" y1="-0.353553" x2="284.671" y2="283.964" stroke="#383057"/> +<line id="Line 3126" y1="-0.5" x2="402.085" y2="-0.5" transform="matrix(-0.707107 0.707107 0.707107 0.707107 284.316 773.91)" stroke="#383057"/> +<line id="Line 3127" y1="-0.5" x2="402.085" y2="-0.5" transform="matrix(-0.707107 0.707107 0.707107 0.707107 1058.23 0)" stroke="#383057"/> +<line id="Line 3128" x1="774.266" y1="773.557" x2="1058.58" y2="1057.87" stroke="#383057"/> +</g> +</g> +<defs> +<clipPath id="clip0"> +<rect width="1058.23" height="1058.23" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/deploy/slc.db.team/src/ui/common.ts b/code/sys.ui/ui-react-components/src/-test/common.ts similarity index 100% rename from deploy/slc.db.team/src/ui/common.ts rename to code/sys.ui/ui-react-components/src/-test/common.ts diff --git a/code/sys.ui/ui-react-components/src/-test/entry.Specs.ts b/code/sys.ui/ui-react-components/src/-test/entry.Specs.ts index 18d00d8cb4..c6de160309 100644 --- a/code/sys.ui/ui-react-components/src/-test/entry.Specs.ts +++ b/code/sys.ui/ui-react-components/src/-test/entry.Specs.ts @@ -1,3 +1,41 @@ -import { type t } from '../common.ts'; +/** + * @module + * DevHarness visual specs. + */ +import type { t } from './common.ts'; +const ns = 'sys.ui.react.component'; -export const Specs = {} as t.SpecImports; +/** + * Components: + */ +export const SpecsComponents = { + [`${ns}: Button`]: () => import('../ui/Button/-SPEC.tsx'), + [`${ns}: Cropmarks`]: () => import('../ui/Cropmarks/-SPEC.tsx'), + [`${ns}: Icon`]: () => import('../ui/Icon/-SPEC.tsx'), + [`${ns}: IFrame`]: () => import('../ui/IFrame/-SPEC.tsx'), + [`${ns}: Image.Svg`]: () => import('../ui/Image.Svg/-SPEC.tsx'), + [`${ns}: ObjectView`]: () => import('../ui/ObjectView/-SPEC.tsx'), + [`${ns}: Panel`]: () => import('../ui/Panel/-SPEC.tsx'), + [`${ns}: Preload`]: () => import('../ui/Preload/-SPEC.tsx'), + [`${ns}: Sheet`]: () => import('../ui/Sheet/-SPEC.tsx'), + [`${ns}: Spinners.Bar`]: () => import('../ui/Spinners.Bar/-SPEC.tsx'), + + [`${ns}: Player.Video`]: () => import('../ui/Player.Video/-SPEC.tsx'), + [`${ns}: Player.Concept`]: () => import('../ui/Player.Concept/-SPEC.tsx'), + [`${ns}: Player.Thumbnails`]: () => import('../ui/Player.Thumbnails/-SPEC.tsx'), + [`${ns}: VimeoBackground`]: () => import('../ui/VimeoBackground/-SPEC.tsx'), +} as t.SpecImports; + +/** + * Samples from external libs: + */ +export const SpecsExternal = { + 'sys.ui.css: @container': () => import('../-sample/-css-container/-SPEC.tsx'), + 'sys.ui.react: useMouse': () => import('../-sample/-dom-useMouse/-SPEC.tsx'), + 'sys.ui.react: useSizeObserver': () => import('../-sample/-dom-useSizeObserver/-SPEC.tsx'), +} as t.SpecImports; + +/** + * Specs + */ +export const Specs = { ...SpecsComponents, ...SpecsExternal } as t.SpecImports; diff --git a/code/sys.ui/ui-react-components/src/-test/entry.tsx b/code/sys.ui/ui-react-components/src/-test/entry.tsx index 2fa8a1f04c..2e8e89e8b7 100644 --- a/code/sys.ui/ui-react-components/src/-test/entry.tsx +++ b/code/sys.ui/ui-react-components/src/-test/entry.tsx @@ -1,31 +1,42 @@ -import { StrictMode } from 'react'; +import React, { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; + import { pkg } from '../common.ts'; /** * Render UI. */ globalThis.document.title = pkg.name; -console.info('Pkg', pkg); +console.info('š· ./entry.tsx ā Pkg:š¦', pkg); -/** - * Main Entry. - */ -async function main() { +export async function main() { const params = new URL(location.href).searchParams; const isDev = params.has('dev') || params.has('d'); + const root = createRoot(document.getElementById('root')!); - console.log('isDev', isDev); + if (isDev) { + const { render, useKeyboard } = await import('@sys/ui-react-devharness'); + const { Specs } = await import('./entry.Specs.ts'); - const { render } = await import('@sys/ui-react-devharness'); - const { Specs } = await import('./entry.Specs.ts'); + const el = await render(pkg, Specs, { hrDepth: 2, style: { Absolute: 0 } }); + function App() { + useKeyboard(); + return el; + } - const el = await render(pkg, Specs, { hrDepth: 2, style: { Absolute: 0 } }); - const root = document.getElementById('root'); - createRoot(root).render(<StrictMode>{el}</StrictMode>); + root.render( + <StrictMode> + <App /> + </StrictMode>, + ); + } else { + const { Splash } = await import('./ui.Splash.tsx'); + root.render( + <StrictMode> + <Splash style={{ Absolute: 0 }} /> + </StrictMode>, + ); + } } -/** - * Run. - */ -main().catch((err) => console.error(`Failed to render`, err)); +main().catch((err) => console.error(`Failed to render DevHarness`, err)); diff --git a/code/sys.ui/ui-react-components/src/-test/mod.ts b/code/sys.ui/ui-react-components/src/-test/mod.ts index a3f41ce278..278cf7f716 100644 --- a/code/sys.ui/ui-react-components/src/-test/mod.ts +++ b/code/sys.ui/ui-react-components/src/-test/mod.ts @@ -1,2 +1,2 @@ -export { describe, expect, expectError, it, Testing } from '@sys/testing/server'; -export * from '../common.ts'; +export { describe, DomMock, expect, expectError, it, Testing } from '@sys/testing/server'; +export * from './common.ts'; diff --git a/code/sys.ui/ui-react-components/src/-test/ui.Splash.tsx b/code/sys.ui/ui-react-components/src/-test/ui.Splash.tsx new file mode 100644 index 0000000000..01b17142cc --- /dev/null +++ b/code/sys.ui/ui-react-components/src/-test/ui.Splash.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { type t, Color, css } from './common.ts'; + +import { useKeyboard } from '@sys/ui-react-devharness'; + +export type SplashProps = { + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +export const Splash: React.FC<SplashProps> = (props) => { + const href = '?dev'; + useKeyboard(); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + fontFamily: 'sans-serif', + display: 'grid', + placeItems: 'center', + color: theme.fg, + }), + a: css({ + textDecoration: 'none', + fontSize: 30, + color: Color.BLUE, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <a href={href} className={styles.a.class}>{`š· ${href}`}</a> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/.test.ts b/code/sys.ui/ui-react-components/src/.test.ts index 25caefb420..7348c03d0a 100644 --- a/code/sys.ui/ui-react-components/src/.test.ts +++ b/code/sys.ui/ui-react-components/src/.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, Pkg, pkg } from './-test.ts'; +import { type t, describe, it, expect, Pkg, pkg } from './-test.ts'; describe(`module: ${Pkg.toString(pkg)}`, () => { it('exists', () => { diff --git a/code/sys.ui/ui-react-components/src/common/libs.ts b/code/sys.ui/ui-react-components/src/common/libs.ts index 6f2db1bc06..7ab8336c72 100644 --- a/code/sys.ui/ui-react-components/src/common/libs.ts +++ b/code/sys.ui/ui-react-components/src/common/libs.ts @@ -1 +1,11 @@ -export { Err, Pkg, rx, Time } from '@sys/std'; +import { motion as Motion } from 'motion/react'; +export { AnimatePresence } from 'motion/react'; +export { Motion as M, Motion }; + +/** + * System + */ +export { Err, Is, isRecord, Path, Pkg, rx, slug, Str, Time, Timestamp } from '@sys/std'; +export { Color, css, Style } from '@sys/ui-css'; +export { Keyboard } from '@sys/ui-dom'; +export { Signal, useSizeObserver, useIsTouchSupported } from '@sys/ui-react'; diff --git a/code/sys.ui/ui-react-components/src/common/t.ts b/code/sys.ui/ui-react-components/src/common/t.ts index b9ff427b14..25fdaf695c 100644 --- a/code/sys.ui/ui-react-components/src/common/t.ts +++ b/code/sys.ui/ui-react-components/src/common/t.ts @@ -1,3 +1,16 @@ -export type { SpecImports } from '@sys/testing/t'; +export type { ReactNode } from 'react'; + export type * from '@sys/types'; + +export type { SpecImports } from '@sys/testing/t'; +export type { + CssEdgesInput, + CssInput, + CssMarginArray, + CssMarginInput, + CssProps, + CssValue, +} from '@sys/ui-css/t'; +export type { ExtractSignalValue, ReadonlySignal, Signal } from '@sys/ui-react/t'; + export type * from '../types.ts'; diff --git a/code/sys.ui/ui-react-components/src/mod.ts b/code/sys.ui/ui-react-components/src/mod.ts index d1cb0441e4..18e4cf6087 100644 --- a/code/sys.ui/ui-react-components/src/mod.ts +++ b/code/sys.ui/ui-react-components/src/mod.ts @@ -1,16 +1,31 @@ /** * @module - * Common UI Components + * Library of common `<React>` components. + * + * @example + * ```ts + * import { Foo } from '@sys/ui-react-components'; * ``` */ export { pkg } from './pkg.ts'; -/** - * Module types. - */ +/** Module types. */ export type * as t from './types.ts'; /** - * Library. + * Components. */ -export { Foo } from './ui/Foo.tsx'; +export { Button } from './ui/Button/mod.ts'; +export { Cropmarks } from './ui/Cropmarks/mod.ts'; +export { Icon } from './ui/Icon/mod.ts'; +export { Svg } from './ui/Image.Svg/mod.ts'; +export { ObjectView } from './ui/ObjectView/mod.ts'; +export { Panel } from './ui/Panel/mod.ts'; +export { Preload } from './ui/Preload/mod.ts'; +export { Sheet } from './ui/Sheet/mod.ts'; +export { Spinners } from './ui/Spinners/mod.ts'; + +export { ConceptPlayer } from './ui/Player.Concept/mod.ts'; +export { VideoPlayer } from './ui/Player.Video/mod.ts'; +export { Player } from './ui/Player/mod.ts'; +export { VimeoBackground } from './ui/VimeoBackground/mod.ts'; diff --git a/code/sys.ui/ui-react-components/src/pkg.ts b/code/sys.ui/ui-react-components/src/pkg.ts index 79cb3f9c80..a072fa9f7b 100644 --- a/code/sys.ui/ui-react-components/src/pkg.ts +++ b/code/sys.ui/ui-react-components/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/ui-react-components', version: '0.0.41' }; diff --git a/code/sys.ui/ui-react-components/src/types.ts b/code/sys.ui/ui-react-components/src/types.ts index 0f2f882807..cf8a421c86 100644 --- a/code/sys.ui/ui-react-components/src/types.ts +++ b/code/sys.ui/ui-react-components/src/types.ts @@ -2,4 +2,19 @@ * @module * Module types. */ -export {}; +export type * from './ui/Button/t.ts'; +export type * from './ui/Cropmarks/t.ts'; +export type * from './ui/Icon/t.ts'; +export type * from './ui/IFrame/t.ts'; +export type * from './ui/Image.Svg/t.ts'; +export type * from './ui/ObjectView/t.ts'; +export type * from './ui/Panel/t.ts'; +export type * from './ui/Player.Concept/t.ts'; +export type * from './ui/Player.Thumbnails/t.ts'; +export type * from './ui/Player.Video/t.ts'; +export type * from './ui/Player/t.ts'; +export type * from './ui/Preload/t.ts'; +export type * from './ui/Sheet/t.ts'; +export type * from './ui/Spinners.Bar/t.ts'; +export type * from './ui/Spinners/t.ts'; +export type * from './ui/VimeoBackground/t.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/-test.ui.ts b/code/sys.ui/ui-react-components/src/ui/-test.ui.ts new file mode 100644 index 0000000000..cffdc99448 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/-test.ui.ts @@ -0,0 +1,16 @@ +/** + * @module + * Testing tools running in the browser/ui. + */ +export { expect } from '@sys/std/testing'; +export { Dev, Spec } from '@sys/ui-react-devharness'; +export * from '../common.ts'; + +/** + * Constants: + */ +export const VIMEO = { + 'app/tubes': 499921561, + 'stock/running': 287903693, // https://vimeo.com/stock/clip-287903693-silhouette-woman-running-on-beach-waves-splashing-female-athlete-runner-exercising-sprinting-intense-workout-on-rough-ocean-seas + 'public/helvetica': 73809723, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Button/--SPEC.CopyButton.tsx_ b/code/sys.ui/ui-react-components/src/ui/Button/--SPEC.CopyButton.tsx_ new file mode 100644 index 0000000000..c63e534211 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/--SPEC.CopyButton.tsx_ @@ -0,0 +1,95 @@ +import { Button } from '.'; +import { Dev, slug, type t } from '../../test.ui'; + +const DEFAULTS = Button.DEFAULTS; +type T = { props: t.CopyButtonProps }; +const initial: T = { props: {} }; + +/** + * Spec + */ +const name = Button.Copy.displayName ?? ''; + +export default Dev.describe(name, (e) => { + type LocalStore = Pick<t.CopyButtonProps, 'enabled' | 'spinning' | 'theme'>; + const localstore = Dev.LocalStorage<LocalStore>('dev:sys.ui.common.Button.Copy'); + const local = localstore.object({ + enabled: DEFAULTS.enabled, + spinning: DEFAULTS.spinning, + theme: undefined, + }); + + e.it('ui:init', async (e) => { + const ctx = Dev.ctx(e); + const dev = Dev.tools<T>(e, initial); + + const state = await ctx.state<T>(initial); + await state.change((d) => { + d.props.enabled = local.enabled; + d.props.spinning = local.spinning; + d.props.theme = local.theme; + }); + + ctx.debug.width(330); + ctx.subject + .backgroundColor(1) + .display('grid') + .render<T>((e) => { + const { props } = e.state; + Dev.Theme.background(ctx, props.theme, 1); + + return ( + <Button.Copy + {...props} + onClick={(e) => console.info('ā”ļø onClick')} + onCopy={(e) => { + console.info('ā”ļø onCopy', e); + e.copy(`My Text (${slug()})`); + // e.delay(1200); + // e.message('Foobar'); + }} + > + {'My Text Button'} + </Button.Copy> + ); + }); + }); + + e.it('ui:debug', async (e) => { + const dev = Dev.tools<T>(e, initial); + const state = await dev.state(); + + dev.section('Properties', (dev) => { + Dev.Theme.switch(dev, ['props', 'theme'], (next) => (local.theme = next)); + + dev.hr(-1, 5); + + dev.boolean((btn) => { + const value = (state: T) => Boolean(state.props.enabled); + btn + .label((e) => `enabled`) + .value((e) => value(e.state)) + .onClick((e) => e.change((d) => (local.enabled = Dev.toggle(d.props, 'enabled')))); + }); + + dev.boolean((btn) => { + const value = (state: T) => Boolean(state.props.spinning); + btn + .label((e) => `spinning`) + .value((e) => value(e.state)) + .onClick((e) => e.change((d) => (local.spinning = Dev.toggle(d.props, 'spinning')))); + }); + }); + + dev.hr(5, 20); + }); + + e.it('ui:footer', async (e) => { + const dev = Dev.tools<T>(e, initial); + const state = await dev.state(); + dev.footer.border(-0.1).render<T>((e) => { + const data = e.state; + return <Dev.Object name={name} data={data} expand={1} />; + }); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Button/--SPEC.tsx_ b/code/sys.ui/ui-react-components/src/ui/Button/--SPEC.tsx_ new file mode 100644 index 0000000000..70058c743f --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/--SPEC.tsx_ @@ -0,0 +1,195 @@ +import { Button } from '.'; +import { Delete, Dev, Pkg, type t } from '../../test.ui'; + +const DEFAULTS = Button.DEFAULTS; + +type T = { + props: t.ButtonProps; + debug: { useLabel?: boolean; padding?: boolean }; +}; +const initial: T = { + props: {}, + debug: {}, +}; + +const name = Button.displayName ?? 'Unknown'; +export default Dev.describe(name, (e) => { + type LocalStore = T['debug'] & + Pick< + t.ButtonProps, + | 'theme' + | 'enabled' + | 'active' + | 'block' + | 'spinning' + | 'tooltip' + | 'label' + | 'isOver' + | 'isDown' + >; + const localstore = Dev.LocalStorage<LocalStore>(`dev:${Pkg.name}.${name}`); + const local = localstore.object({ + theme: undefined, + useLabel: true, + padding: false, + enabled: DEFAULTS.enabled, + active: DEFAULTS.active, + block: DEFAULTS.block, + spinning: DEFAULTS.spinning, + tooltip: 'My Button', + label: 'Hello-š·', + isOver: undefined, + isDown: undefined, + }); + + e.it('init', async (e) => { + const ctx = Dev.ctx(e); + const state = await ctx.state<T>(initial); + + await state.change((d) => { + d.props.theme = local.theme; + d.props.enabled = local.enabled; + d.props.active = local.active; + d.props.block = local.block; + d.props.spinning = local.spinning; + d.props.tooltip = local.tooltip; + d.props.label = local.label; + d.props.isOver = local.isOver; + d.props.isDown = local.isDown; + d.debug.useLabel = local.useLabel; + d.debug.padding = local.padding; + }); + + ctx.subject.display('grid').render<T>((e) => { + const { debug } = e.state; + Dev.Theme.background(ctx, e.state.props.theme); + + const props = { + ...e.state.props, + padding: debug.padding ? 20 : undefined, + }; + + if (debug.useLabel) { + props.label = 'Label-š·'; + props.children = undefined; + } else { + props.label = undefined; + props.children = <div>{'My Child Element'}</div>; + } + + return ( + <Button + {...props} + onMouse={(e) => console.info(`ā”ļø onMouse`, e)} + onClick={(e) => { + console.info('ā”ļø onClick', e); + // state.change((d) => Dev.toggle(d.props, 'spinning')); + }} + /> + ); + }); + }); + + e.it('ui:debug', async (e) => { + const dev = Dev.tools<T>(e, initial); + + dev.section('Properties', (dev) => { + Dev.Theme.switch(dev, ['props', 'theme'], (next) => (local.theme = next)); + + dev.boolean((btn) => + btn + .label('enabled') + .value((e) => e.state.props.enabled) + .onClick((e) => e.change((d) => (local.enabled = Dev.toggle(d.props, 'enabled')))), + ); + + dev.boolean((btn) => + btn + .label('active') + .value((e) => e.state.props.active) + .onClick((e) => e.change((d) => (local.active = Dev.toggle(d.props, 'active')))), + ); + + dev.boolean((btn) => + btn + .label((e) => 'spinning') + .value((e) => e.state.props.spinning) + .onClick((e) => e.change((d) => (local.spinning = Dev.toggle(d.props, 'spinning')))), + ); + + dev.hr(-1, 5); + + dev.boolean((btn) => { + const value = (state: T) => !!state.props.isOver; + btn + .label((e) => `isOver (force)`) + .value((e) => value(e.state)) + .onClick((e) => e.change((d) => (local.isOver = Dev.toggle(d.props, 'isOver')))); + }); + dev.boolean((btn) => { + const value = (state: T) => !!state.props.isDown; + btn + .label((e) => `isDown (force)`) + .value((e) => value(e.state)) + .onClick((e) => e.change((d) => (local.isDown = Dev.toggle(d.props, 'isDown')))); + }); + + dev.hr(-1, 5); + + dev.boolean((btn) => + btn + .label('block') + .value((e) => e.state.props.block) + .onClick((e) => e.change((d) => (local.block = Dev.toggle(d.props, 'block')))), + ); + + dev.boolean((btn) => { + const value = (state: T) => !!state.props.overlay; + btn + .label((e) => `overlay (Element)`) + .value((e) => value(e.state)) + .onClick((e) => { + e.change((d) => { + const el = <div style={{ fontSize: 11 }}>{`Overlay š·`}</div>; + d.props.overlay = d.props.overlay ? undefined : el; + }); + }); + }); + }); + + dev.hr(5, 20); + + dev.section('Debug', (dev) => { + dev.button((btn) => + btn + .label('content: <Element>') + .right((e) => (!e.state.debug.useLabel ? 'ā current' : '')) + .onClick((e) => e.change((d) => (local.useLabel = d.debug.useLabel = false))), + ); + + dev.button((btn) => + btn + .label('content: "label" property (string)') + .right((e) => (e.state.debug.useLabel ? 'ā current' : '')) + .onClick((e) => e.change((d) => (local.useLabel = d.debug.useLabel = true))), + ); + + dev.hr(-1, 5); + + dev.boolean((btn) => + btn + .label((e) => `padding`) + .value((e) => Boolean(e.state.debug.padding)) + .onClick((e) => e.change((d) => (local.padding = Dev.toggle(d.debug, 'padding')))), + ); + }); + }); + + e.it('ui:footer', async (e) => { + const dev = Dev.tools<T>(e, initial); + dev.footer.border(-0.1).render<T>((e) => { + const data = { props: Delete.undefined(e.state.props) }; + return <Dev.Object name={name} data={data} expand={1} />; + }); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Button/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Button/-SPEC.Debug.tsx new file mode 100644 index 0000000000..2f61d5d788 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/-SPEC.Debug.tsx @@ -0,0 +1,31 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { type t, Color, css, Signal, DEFAULTS, rx } from './common.ts'; + +export type DebugProps = { + ctx: {}; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = DebugProps; + +/** + * Component + */ +export const Debug: React.FC<P> = (props) => { + const {} = props; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ color: theme.fg }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div>{`š· Debug`}</div> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Button/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Button/-SPEC.tsx new file mode 100644 index 0000000000..db596e62b9 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/-SPEC.tsx @@ -0,0 +1,23 @@ +import { Spec } from '../-test.ui.ts'; +import { Debug } from './-SPEC.Debug.tsx'; +import { Button } from './mod.ts'; + +export default Spec.describe('Button', (e) => { + e.it('init', async (e) => { + const ctx = Spec.ctx(e); + ctx.subject.size([224, null]).render((e) => { + return ( + <Button + onClick={(e) => console.info(`ā”ļø onClick:`, e)} + onMouseDown={(e) => console.info(`ā”ļø onMouseDown:`, e)} + onMouseUp={(e) => console.info(`ā”ļø onMouseUp:`, e)} + >{`š Hello Button`}</Button> + ); + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug ctx={{}} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Button/common.ts b/code/sys.ui/ui-react-components/src/ui/Button/common.ts new file mode 100644 index 0000000000..0d0a03e9fe --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/common.ts @@ -0,0 +1,19 @@ +import { pkg } from '../common.ts'; +export * from '../common.ts'; + +/** + * Constants + */ +const name = 'Button'; +const displayName = `${pkg.name}:${name}`; + +export const DEFAULTS = { + name, + displayName, + enabled: true, + active: true, + block: false, + disabledOpacity: 0.3, + userSelect: false, + pressedOffset: [0, 1] as [number, number], +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Button/mod.ts b/code/sys.ui/ui-react-components/src/ui/Button/mod.ts new file mode 100644 index 0000000000..96209b7d8d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Clickable buttons. + */ +export { Button } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Button/t.ts b/code/sys.ui/ui-react-components/src/ui/Button/t.ts new file mode 100644 index 0000000000..9a80d3d7e9 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/t.ts @@ -0,0 +1,62 @@ +import type React from 'react'; +import type { t } from './common.ts'; + +type MouseHandler = React.MouseEventHandler; +type Content = JSX.Element | string | number | false; + +/** + * <Component>: Button (simple clickable element). + */ +export type ButtonProps = { + children?: Content; + label?: string | (() => string); + enabled?: boolean; + active?: boolean; + block?: boolean; + tooltip?: string; + + isOver?: boolean; // force the button into an "is-over" state. + isDown?: boolean; // force the button into an "is-down" state. + + theme?: t.CommonTheme; + style?: t.CssInput; + margin?: t.CssEdgesInput; + padding?: t.CssEdgesInput; + minWidth?: number; + maxWidth?: number; + disabledOpacity?: number; + userSelect?: boolean; + pressedOffset?: [number, number]; + + /** Subscribe to signals that cause the button to redraw. */ + subscribe?: () => void; + + onClick?: MouseHandler; + onMouseDown?: MouseHandler; + onMouseUp?: MouseHandler; + onMouseEnter?: MouseHandler; + onMouseLeave?: MouseHandler; + onDoubleClick?: MouseHandler; + onMouse?: t.ButtonMouseHandler; +}; + +/** + * Events + */ +export type ButtonMouseHandler = (e: ButtonMouseHandlerArgs) => void; +export type ButtonMouseHandlerArgs = { + isDown: boolean; + isOver: boolean; + isEnabled: boolean; + action: 'MouseEnter' | 'MouseLeave' | 'MouseDown' | 'MouseUp'; + event: React.MouseEvent; +}; + +export type ButtonCopyHandler = (e: ButtonCopyHandlerArgs) => void; +export type ButtonCopyHandlerArgs = { + message(value: Content): ButtonCopyHandlerArgs; + fontSize(value: number): ButtonCopyHandlerArgs; + opacity(value: number): ButtonCopyHandlerArgs; + delay(value: t.Msecs): ButtonCopyHandlerArgs; + copy(value?: string | number): Promise<void>; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Button/u.events.ts b/code/sys.ui/ui-react-components/src/ui/Button/u.events.ts new file mode 100644 index 0000000000..0ffca8aa44 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/u.events.ts @@ -0,0 +1,155 @@ +import type React from 'react'; +import { type t, DEFAULTS as D } from './common.ts'; + +type P = t.ButtonProps; +type S = [boolean, (next: boolean) => void]; + +/** + * An object containing the current state of the button + * used to pass into handlers. + */ +export type EventState = { + readonly props: P; + over: boolean; + down: boolean; +}; + +/** + * EventState Factory: + */ +export function toEventState(props: P, over: S, down: S): EventState { + return { + props, + get over() { + return over[0]; + }, + set over(value) { + over[1](value); + }, + get down() { + return down[0]; + }, + set down(value) { + down[1](value); + }, + }; +} + +/** + * Events for desktop devices. + */ +const Desktop = { + over(state: EventState, isOver: boolean): React.MouseEventHandler { + return (e) => { + const props = wrangle.props(state); + if (!props.active) return; + state.over = isOver; + if (!isOver && state.down) state.down = false; + if (props.enabled) { + if (isOver) props.onMouseEnter?.(e); + if (!isOver) props.onMouseLeave?.(e); + } + + const isDown = !isOver ? false : state.down; + const isEnabled = props.enabled; + const action = isOver ? 'MouseEnter' : 'MouseLeave'; + props.onMouse?.({ event: e, isOver, isDown, isEnabled, action }); + }; + }, + + down(state: EventState, isDown: boolean): React.MouseEventHandler { + return (e) => { + const props = wrangle.props(state); + if (!props.active) return; + + state.down = isDown; + if (props.enabled) { + if (isDown) props.onMouseDown?.(e); + if (!isDown) props.onMouseUp?.(e); + if (!isDown) props.onClick?.(e); + } + + const isOver = state.over; + const isEnabled = props.enabled; + const action = isDown ? 'MouseDown' : 'MouseUp'; + props.onMouse?.({ event: e, isOver, isDown, isEnabled, action }); + }; + }, +} as const; + +/** + * Events for mobile devices. + */ +const Mobile = { + down(state: EventState, isDown: boolean): React.TouchEventHandler { + return (e: React.TouchEvent) => { + const props = wrangle.props(state); + if (!props.active) return; + + state.down = isDown; + const event = wrangle.asMouseEvent(e); + if (props.enabled) { + if (isDown && props.onMouseDown) props.onMouseDown(event); + if (!isDown && props.onMouseUp) props.onMouseUp(event); + if (!isDown && props.onClick) props.onClick(event); + } + + const isEnabled = props.enabled; + const action = isDown ? 'MouseDown' : 'MouseUp'; + props.onMouse?.({ event, isOver: isDown, isDown, isEnabled, action }); + }; + }, +} as const; + +/** + * Event tools: + */ +export const Event = { + Desktop, + Mobile, + handlers(state: EventState, isMobile: boolean) { + return isMobile + ? { + onTouchStart: Event.Mobile.down(state, true), + onTouchEnd: Event.Mobile.down(state, false), + onTouchCancel: Event.Mobile.down(state, false), + } + : { + onMouseEnter: Event.Desktop.over(state, true), + onMouseLeave: Event.Desktop.over(state, false), + onMouseDown: Event.Desktop.down(state, true), + onMouseUp: Event.Desktop.down(state, false), + onClick(e: React.MouseEvent) { + // NB: "onClick" is sythetically derived from mouse-down/up events + // so prevent the native onClick event from confusingly propgating + // a different version of "onClick" up in the DOM hierarchy. + e.stopPropagation(); + }, + }; + }, +} as const; + +/** + * Helpers + */ +const wrangle = { + props(state: EventState) { + const { enabled = D.enabled, active = D.active } = state.props; + return { ...state.props, enabled, active }; + }, + + asMouseEvent(e: React.TouchEvent): React.MouseEvent { + const touch = e.changedTouches[0]; + return { + ...e, + button: 0, + buttons: 1, + clientX: touch.clientX, + clientY: touch.clientY, + pageX: touch.pageX, + pageY: touch.pageY, + screenX: touch.screenX, + screenY: touch.screenY, + } as unknown as React.MouseEvent; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Button/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Button/ui.tsx new file mode 100644 index 0000000000..2cb8f59be0 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Button/ui.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useState } from 'react'; +import { type t, Color, css, DEFAULTS, Style, useIsTouchSupported, Signal } from './common.ts'; +import { Event, toEventState } from './u.events.ts'; + +type P = t.ButtonProps; + +/** + * Component + */ +export const Button: React.FC<P> = (props) => { + const { + enabled = DEFAULTS.enabled, + active = DEFAULTS.active, + block = DEFAULTS.block, + disabledOpacity = DEFAULTS.disabledOpacity, + userSelect = DEFAULTS.userSelect, + pressedOffset = DEFAULTS.pressedOffset, + } = props; + const isBlurred = false; + const isEnabled = enabled; + const label = wrangle.label(props); + + const over = useState(false); + const down = useState(false); + const [isOver, setOver] = over; + const [isDown, setDown] = down; + + const isMobile = useIsTouchSupported(); + const eventState = toEventState(props, over, down); + + /** + * Effects: + */ + Signal.useRedrawEffect(() => props.subscribe?.()); + useEffect(() => { + if (!active) { + setDown(false); + setOver(false); + } + }, [active]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + ...Style.toMargins(props.margin), + ...Style.toPadding(props.padding), + position: 'relative', + display: block ? 'block' : 'inline-block', + minWidth: props.minWidth, + maxWidth: props.maxWidth, + opacity: enabled ? 1 : disabledOpacity, + cursor: enabled && active && !isBlurred ? 'pointer' : 'default', + userSelect: userSelect ? 'auto' : 'none', + }), + body: css({ + color: wrangle.color({ + isEnabled, + isOver: !!(isOver || props.isOver), + theme: theme.name, + }), + transform: wrangle.pressedOffset({ + isEnabled, + isOver: !!(isOver || props.isOver), + isDown: !!(isDown || props.isDown), + pressedOffset, + }), + opacity: isBlurred ? 0.15 : 1, + filter: `blur(${isBlurred ? 3 : 0}px) grayscale(${isBlurred ? 100 : 0}%)`, + transition: 'opacity 0.1s ease', + }), + }; + + return ( + <div + role={'button'} + title={props.tooltip} + className={css(styles.base, props.style).class} + onDoubleClick={props.onDoubleClick} + {...Event.handlers(eventState, isMobile)} + > + <div className={styles.body.class}> + {label && <div>{label}</div>} + {props.children} + </div> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + color(args: { isEnabled: boolean; isOver?: boolean; theme?: t.CommonTheme }) { + const color = args.theme === 'Dark' ? Color.WHITE : Color.DARK; + if (!args.isEnabled) return color; + return args.isOver ? Color.BLUE : color; + }, + + pressedOffset(args: { + isEnabled: boolean; + isOver: boolean; + isDown: boolean; + pressedOffset: [number, number]; + }) { + const { isEnabled, isOver, isDown, pressedOffset } = args; + if (!isEnabled) return undefined; + if (!isOver) return undefined; + if (!isDown) return undefined; + return `translateX(${pressedOffset[0]}px) translateY(${pressedOffset[1]}px)`; + }, + + label(props: P) { + const { label } = props; + return typeof label === 'function' ? label() : label; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.Debug.tsx new file mode 100644 index 0000000000..5f66070ab9 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.Debug.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, Color, css, Signal } from './common.ts'; + +import { type DebugSignals } from './-SPEC.signals.tsx'; + +/** + * Component: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => { + p.theme.value; + p.size.value; + p.subjectOnly.value; + }); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`subjectOnly: ${p.subjectOnly}`} + onClick={() => Signal.toggle(p.subjectOnly)} + /> + + <hr /> + + <Button block label={`set size: <undefined>`} onClick={() => (p.size.value = undefined)} /> + <Button + block + label={`set size: "fill"`} + onClick={() => { + p.size.value = { mode: 'fill', x: true, y: true, margin: [40, 40, 40, 40] }; + }} + /> + <Button + block + label={`set size: "center"`} + onClick={() => { + p.size.value = { mode: 'center' }; + }} + /> + + <hr /> + + {p.size.value?.mode === 'center' && <DebugCenter debug={debug} />} + {p.size.value?.mode === 'fill' && <DebugFill debug={debug} />} + </div> + ); +}; + +/** + * Component: + */ +export type DebugFillProps = { debug: DebugSignals; style?: t.CssInput }; +export const DebugFill: React.FC<DebugFillProps> = (props) => { + const { debug } = props; + const p = debug.props; + const size = p.size.value; + if (size?.mode !== 'fill') return null; + + /** + * Render: + */ + const styles = { + base: css({}), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`size.margin: ${size.margin ?? '<undefined>'}`} + onClick={() => { + const current = size.margin[0]; + let next = [40, 40, 40, 40]; + if (current === 40) next = [80, 60, 30, 10]; + if (current === 80) next = [100, 100, 100, 100]; + if (current === 100) next = [40, 40, 40, 40]; + p.size.value = { ...size, margin: next as t.CssMarginArray }; + }} + /> + + <Button + block + label={`size.x: ${size.x}`} + onClick={() => (p.size.value = { ...size, x: !size.x })} + /> + + <Button + block + label={`size.y: ${size.y}`} + onClick={() => (p.size.value = { ...size, y: !size.y })} + /> + </div> + ); +}; + +/** + * Component: + */ +export type DebugCenterProps = { + debug: DebugSignals; + style?: t.CssInput; +}; +export const DebugCenter: React.FC<DebugCenterProps> = (props) => { + const { debug } = props; + const p = debug.props; + + const size = p.size.value; + if (size?.mode !== 'center') return null; + + /** + * Render: + */ + const styles = { + base: css({}), + }; + + const sizes = [0, 300, 500]; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`size.width: ${size.width}`} + onClick={() => { + const next = cycleNumber(size.width ?? 0, sizes); + p.size.value = { ...size, width: next }; + }} + /> + + <Button + block + label={`size.height: ${size.height}`} + onClick={() => { + const next = cycleNumber(size.height ?? 0, sizes); + p.size.value = { ...size, height: next }; + }} + /> + </div> + ); +}; + +/** + * Helpers + */ +export function cycleNumber(current: number, values: number[]): number { + const index = values.indexOf(current); + const nextIndex = index >= 0 ? (index + 1) % values.length : 0; + return values[nextIndex]; +} diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.signals.tsx b/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.signals.tsx new file mode 100644 index 0000000000..7b76de02f6 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.signals.tsx @@ -0,0 +1,30 @@ +import { type t, Signal } from './common.ts'; + +/** + * Types: + */ +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + type P = t.CropmarksProps; + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + size: s<P['size']>(), + subjectOnly: s<P['subjectOnly']>(false), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.size.value; + p.subjectOnly.value; + }, + }; + init?.(api); + return api; +} diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.tsx new file mode 100644 index 0000000000..d2b5a4f690 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/-SPEC.tsx @@ -0,0 +1,41 @@ +import { Dev, Signal, Spec, css } from '../-test.ui.ts'; +import { Debug } from './-SPEC.Debug.tsx'; +import { createDebugSignals } from './-SPEC.signals.tsx'; +import { Cropmarks } from './mod.ts'; + +export default Spec.describe('Cropmarks', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + const styles = { + subject: css({ + backgroundColor: 'rgba(255, 0, 0, 0.06)' /* RED */, + overflow: 'hidden', + padding: 8, + }), + }; + + ctx.subject + .size('fill') + .display('grid') + .render((e) => ( + <Cropmarks theme={p.theme.value} size={p.size.value} subjectOnly={p.subjectOnly.value}> + <div className={styles.subject.class}>{'š· Hello Cropmarks'}</div> + </Cropmarks> + )); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/common.ts b/code/sys.ui/ui-react-components/src/ui/Cropmarks/common.ts new file mode 100644 index 0000000000..95e7fd1bf8 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/common.ts @@ -0,0 +1,7 @@ +import { type t } from '../common.ts'; +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/mod.ts b/code/sys.ui/ui-react-components/src/ui/Cropmarks/mod.ts new file mode 100644 index 0000000000..3f88932779 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { Cropmarks } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/t.ts b/code/sys.ui/ui-react-components/src/ui/Cropmarks/t.ts new file mode 100644 index 0000000000..f5526664ad --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/t.ts @@ -0,0 +1,38 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type CropmarksProps = { + children?: t.ReactNode; + subjectOnly?: boolean; + size?: t.CropmarksSize; + borderWidth?: number; + borderOpacity?: number; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +/** + * The size configuration of the <Cropmarks>. + */ +export type CropmarksSize = CropmarksSizeCenter | CropmarksSizeFill; +/** The display-mode flag for <Cropmarks> size. */ +export type CropmarksSizeMode = CropmarksSize['mode']; + +/** The subject is centered within the <Cropmaks> host. */ +export type CropmarksSizeCenter = { + mode: 'center'; + width?: number; + height?: number; +}; +/** The subject fills the <Cropmaks> host. */ +export type CropmarksSizeFill = { + mode: 'fill'; + /** Pixel margin around the */ + margin: t.CssMarginArray; + /** Fills the X (horizontal) plane. */ + x: boolean; + /** Fills the Y (vertical) plane. */ + y: boolean; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/u.ts b/code/sys.ui/ui-react-components/src/ui/Cropmarks/u.ts new file mode 100644 index 0000000000..9da365e112 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/u.ts @@ -0,0 +1,26 @@ +import { type t, Style } from './common.ts'; + +export const Wrangle = { + componentSize(value?: t.CropmarksSize) { + let width: number | undefined; + let height: number | undefined; + + if (!value) return { width, height } as const; + if (value.mode === 'fill') return { width, height } as const; + + width = value.width; + height = value.height; + return { width, height } as const; + }, + + fillMargin(value?: t.CropmarksSize) { + const DEFAULT = 40; + if (!value) return Wrangle.asMargin(DEFAULT); + if (value.mode !== 'fill') return Wrangle.asMargin(DEFAULT); + return value.margin; + }, + + asMargin(value: number): t.CssMarginArray { + return Style.Edges.toArray(value ?? 0); + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Cropmarks/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Cropmarks/ui.tsx new file mode 100644 index 0000000000..02cf21ab3d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Cropmarks/ui.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { type t, Color, css } from './common.ts'; +import { Wrangle } from './u.ts'; + +type P = t.CropmarksProps; + +export const Cropmarks: React.FC<P> = (props) => { + const { size, subjectOnly = false } = props; + + if (subjectOnly) return props.children; + + const fillMargin = Wrangle.fillMargin(size); + const sizeMode: t.CropmarksSizeMode = size?.mode ?? 'center'; + const is = { + x: size?.mode === 'fill' && size.x && !size.y, + y: size?.mode === 'fill' && !size.x && size.y, + } as const; + + const Grid = { + Fill: { + columns: `[left] ${fillMargin[3]}px [body-x] 1fr [right] ${fillMargin[1]}px`, + rows: `[top] ${fillMargin[0]}px [body-y] 1fr [bottom] ${fillMargin[2]}px`, + }, + Center: { + columns: `[left] 1fr [body-x] auto [right] 1fr`, + rows: `[top] 1fr [body-y] auto [bottom] 1fr`, + }, + } as const; + + /** + * Render: + */ + + const fill = { + gridTemplateColumns: is.y ? Grid.Center.columns : Grid.Fill.columns, + gridTemplateRows: is.x ? Grid.Center.rows : Grid.Fill.rows, + }; + const center = { + gridTemplateColumns: Grid.Center.columns, + gridTemplateRows: Grid.Center.rows, + }; + const grid = { + center: sizeMode === 'center' && center, + fill: sizeMode === 'fill' && fill, + }; + + const border = wrangle.border(props); + const borderLeft = border; + const borderRight = border; + const borderTop = border; + const borderBottom = border; + + const styles = { + base: css({ position: 'relative', display: 'grid' }), + block: css({}), + subject: css({ + position: 'relative', + border, + width: size?.mode === 'center' ? size.width : undefined, + height: size?.mode === 'center' ? size.height : undefined, + display: 'grid', + }), + }; + + const className = css( + styles.base, + sizeMode === 'center' ? grid.center : undefined, + sizeMode === 'fill' ? grid.fill : undefined, + props.style, + ).class; + + return ( + <div className={className}> + <div className={styles.block.class} /> + <div className={css(styles.block, { borderLeft, borderRight }).class} /> + <div className={styles.block.class} /> + <div className={css(styles.block, { borderTop, borderBottom }).class} /> + <div className={styles.subject.class}>{props.children}</div> + <div className={css(styles.block, { borderTop, borderBottom }).class} /> + <div className={styles.block.class} /> + <div className={css(styles.block, { borderLeft, borderRight }).class} /> + <div className={styles.block.class} /> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + border(props: P) { + const theme = Color.theme(props.theme); + const { borderWidth = 1, borderOpacity } = props; + const opacity = typeof borderOpacity === 'number' ? borderOpacity : theme.is.dark ? 0.1 : 0.07; + if (borderWidth <= 0 || opacity <= 0) return; + return `solid ${borderWidth}px ${Color.alpha(theme.fg, opacity)}`; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Foo.tsx b/code/sys.ui/ui-react-components/src/ui/Foo.tsx deleted file mode 100644 index 4034e7059f..0000000000 --- a/code/sys.ui/ui-react-components/src/ui/Foo.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-types="@types/react" -import React from 'react'; -import { pkg } from '../pkg.ts'; - -/** - * Sample component properties. - */ -export type FooProps = { enabled?: boolean }; - -/** - * Sample component. - */ -export const Foo: React.FC<FooProps> = (props) => { - const { enabled = true } = props; - let text = `${pkg.name}@${pkg.version}/ui:<Foo>`; - if (!enabled) text += ' (disabled)'; - return <code>{text}</code>; -}; diff --git a/code/sys.ui/ui-react-components/src/ui/IFrame/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/IFrame/-SPEC.Debug.tsx new file mode 100644 index 0000000000..380d0052e3 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/IFrame/-SPEC.Debug.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, Color, css, Signal, Str } from './common.ts'; + +const local = new URL(`${location.origin}${location.pathname}`); + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + showBackground: s<boolean>(true), + + theme: s<t.CommonTheme>('Light'), + src: s<t.IFrameProps['src']>(local.href), + allowFullScreen: s<t.IFrameProps['allowFullScreen']>(), + loading: s<t.IFrameProps['loading']>(), + sandbox: s<t.IFrameSandbox[] | undefined>(), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.src.value; + p.showBackground.value; + p.allowFullScreen.value; + p.loading.value; + p.sandbox.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<P> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => { + debug.listen(); + }); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + title: css({ fontWeight: 'bold' }), + }; + + const srcLoader = (label: string, src?: t.IFrameProps['src']) => { + return <Button block label={label} onClick={() => (p.src.value = src)} />; + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`show background image: ${p.showBackground}`} + onClick={() => Signal.toggle(p.showBackground)} + /> + + <hr /> + + <Button + block + label={`allowFullScreen: ${p.allowFullScreen}`} + onClick={() => Signal.toggle(p.allowFullScreen)} + /> + + <Button + block + label={`loading: ${p.loading}`} + onClick={() => { + Signal.cycle<t.IFrameProps['loading']>(p.loading, ['eager', 'lazy', undefined]); + }} + /> + + <Button + block + label={`sandbox: ${p.sandbox}`} + onClick={() => { + Signal.cycle<t.IFrameSandbox[] | undefined>(p.sandbox, [ + undefined, + ['allow-popups', 'allow-same-origin'], + ['allow-pointer-lock'], + ]); + }} + /> + + <hr /> + + <div className={styles.title.class}>{`src: ${Str.truncate(String(p.src.value), 30)}`}</div> + {srcLoader(local.host, local.href)} + {srcLoader('Wikipedia: "W3C"', `https://en.wikipedia.org/wiki/World_Wide_Web_Consortium`)} + {srcLoader('Wikipedia: "Foobar" mobile format', `https://en.m.wikipedia.org/wiki/Foobar`)} + <hr /> + {srcLoader('error: google.com (ā blocked)', 'https://google.com')} + <hr /> + {srcLoader('{ html }', { html: '<h1>Hello š<h1>' })} + {srcLoader('<undefined> ā unload', undefined)} + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/IFrame/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/IFrame/-SPEC.tsx new file mode 100644 index 0000000000..e2f7512f49 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/IFrame/-SPEC.tsx @@ -0,0 +1,60 @@ +import { Dev, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Signal } from './common.ts'; +import { IFrame } from './mod.ts'; + +/** + * Sample Data. + */ +const SAMPLE = { + src: 'https://en.wikipedia.org/wiki/World_Wide_Web_Consortium', + allow: `camera; microphone`, +} as const; + +export default Spec.describe('IFrame', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + Dev.Theme.signalEffect(ctx, p.theme); + + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + /** + * Effect: Host Backbground Image + */ + Signal.effect(() => { + const showBackground = p.showBackground.value; + const IMG_HREF = `https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80`; + const url = showBackground ? IMG_HREF : ''; + ctx.host.backgroundImage({ url, opacity: 0.3 }); + }); + + /** + * Render: + */ + ctx.subject + .size('fill') + .display('grid') + .backgroundColor('') + .render((e) => { + return ( + <IFrame + src={p.src.value} + allow={SAMPLE.allow} + onReady={(e) => console.info('ā”ļø onReady:', e)} + onLoad={(e) => console.info('ā”ļø onLoad:', e)} + /> + ); + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/IFrame/common.ts b/code/sys.ui/ui-react-components/src/ui/IFrame/common.ts new file mode 100644 index 0000000000..eddccde9dc --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/IFrame/common.ts @@ -0,0 +1,12 @@ +import { type t } from './common.ts'; +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { + sandbox: true, + get loading(): t.IFrameLoading { + return 'eager'; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/IFrame/mod.ts b/code/sys.ui/ui-react-components/src/ui/IFrame/mod.ts new file mode 100644 index 0000000000..2c4935499b --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/IFrame/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { IFrame } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/IFrame/t.ts b/code/sys.ui/ui-react-components/src/ui/IFrame/t.ts new file mode 100644 index 0000000000..0b2a0f4993 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/IFrame/t.ts @@ -0,0 +1,56 @@ +import type { t } from './common.ts'; +import type { HTMLAttributeReferrerPolicy } from 'react'; + +type HttpPermissionsPolicy = string; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy +type Ref = React.RefObject<HTMLIFrameElement>; + +/** + * Component + */ +export type IFrameProps = { + src?: t.StringUrl | IFrameSrc; + width?: string | number; + height?: string | number; + title?: string; + name?: string; + sandbox?: true | t.IFrameSandbox[]; + allow?: HttpPermissionsPolicy; + allowFullScreen?: boolean; + referrerPolicy?: HTMLAttributeReferrerPolicy | undefined; + loading?: t.IFrameLoading; + style?: t.CssInput; + onReady?: IFrameReadyHandler; + onLoad?: IFrameLoadedEventHandler; +}; + +/** + * Applies extra restrictions to the content in the frame. + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox + */ +export type IFrameSandbox = + | 'allow-downloads-without-user-activation' + | 'allow-downloads' + | 'allow-forms' + | 'allow-modals' + | 'allow-orientation-lock' + | 'allow-pointer-lock' + | 'allow-popups' + | 'allow-popups-to-escape-sandbox' + | 'allow-presentation' + | 'allow-same-origin' + | 'allow-scripts' + | 'allow-storage-access-by-user-activation' + | 'allow-top-navigation' + | 'allow-top-navigation-by-user-activation'; + +export type IFrameLoading = 'eager' | 'lazy'; +export type IFrameSrc = { html?: string; url?: t.StringUrl }; + +/** + * Events + */ +export type IFrameReadyHandler = (e: IFrameReadyHandlerArgs) => void; +export type IFrameReadyHandlerArgs = { ref: Ref }; + +export type IFrameLoadedEventHandler = (e: IFrameLoadedEventHandlerArgs) => void; +export type IFrameLoadedEventHandlerArgs = { href: t.StringUrl; ref: Ref }; diff --git a/code/sys.ui/ui-react-components/src/ui/IFrame/ui.tsx b/code/sys.ui/ui-react-components/src/ui/IFrame/ui.tsx new file mode 100644 index 0000000000..5c0fc92c61 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/IFrame/ui.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useRef } from 'react'; +import { type t, css, DEFAULTS } from './common.ts'; + +export const IFrame: React.FC<t.IFrameProps> = (props) => { + const { width, height, loading = 'eager' } = props; + const content = wrangle.content(props); + const ref = useRef<HTMLIFrameElement>(null); + + /** + * Handlers: + */ + const handleLoad = () => { + let href = content.src ?? ''; + try { + href = ref.current?.contentWindow?.location.href ?? href; + } catch (error) { + // [Ignore]: This will be a cross-origin block. + // Fire the best guess at what the URL is. + } + props.onLoad?.({ ref, href }); + }; + + /** + * Lifecycle: + */ + useEffect(() => props.onReady?.({ ref }), []); + + /** + * Render: + */ + const styles = { + base: css({ position: 'relative', width, height }), + iframe: css({ + Absolute: 0, + width: width ?? '100%', + height: height ?? '100%', + border: 'none', + background: 'transparent', + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + {props.src && ( + <iframe + className={styles.iframe.class} + ref={ref} + src={content.src} + srcDoc={content.html} + title={props.title} + name={props.name} + allow={props.allow} + allowFullScreen={props.allowFullScreen} + referrerPolicy={props.referrerPolicy} + loading={loading} + sandbox={wrangle.sandbox(props)} + onLoad={handleLoad} + /> + )} + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + sandbox(props: t.IFrameProps) { + const { sandbox = DEFAULTS.sandbox } = props; + return Array.isArray(sandbox) ? sandbox.join(' ') : undefined; // NB: <undefined> === all restrictions applied. + }, + + content(props: t.IFrameProps): { src?: string; html?: string } { + if (!props.src) return { src: undefined, html: undefined }; + if (typeof props.src === 'string') return { src: props.src }; + return { src: props.src.url, html: props.src.html }; + }, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.Debug.tsx new file mode 100644 index 0000000000..fe40c17bf8 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.Debug.tsx @@ -0,0 +1,29 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { type t, Color, css, Signal, rx } from './common.ts'; + +export type DebugProps = { ctx: {}; theme?: t.CommonTheme; style?: t.CssValue }; +type P = DebugProps; + +/** + * Component + */ +export const Debug: React.FC<P> = (props) => { + const {} = props; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + backgroundColor: 'rgba(255, 0, 0, 0.1)' /* RED */, + color: theme.fg, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div>{`š· Debug`}</div> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.sample.ts b/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.sample.ts new file mode 100644 index 0000000000..edad5f2325 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.sample.ts @@ -0,0 +1,13 @@ +import { MdFace } from 'react-icons/md'; +import { VscSymbolClass } from 'react-icons/vsc'; +import { Icon } from './mod.ts'; + +const icon = Icon.renderer; + +/** + * Icon collection: + */ +export const Icons = { + Face: icon(MdFace), + Object: icon(VscSymbolClass), +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.tsx new file mode 100644 index 0000000000..800607e5d5 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/-SPEC.tsx @@ -0,0 +1,122 @@ +import { css, Spec, type t } from '../-test.ui.ts'; +import { Debug } from './-SPEC.Debug.tsx'; +import { Icons } from './-SPEC.sample.ts'; + +type T = { + props: t.IconProps; + debug: { inheritColor: boolean }; +}; +// const initial: T = { +// props: { tooltip: 'Hello' }, +// debug: { inheritColor: true }, +// }; + +export default Spec.describe('Icon', (e) => { + e.it('init', async (e) => { + const ctx = Spec.ctx(e); + // await ctx.state<T>(initial); + + ctx.subject + .backgroundColor(1) + .display('grid') + .render<T>((e) => { + // const debug = e.state.debug; + const styles = { + base: css({ + // color: debug.inheritColor ? COLORS.MAGENTA : undefined, // NB: button should inherit this color. + }), + }; + return ( + <div className={styles.base.class}> + <Icons.Face + {...e.state.props} + size={150} + onClick={(e) => console.info(`ā”ļø onClick:`, e)} + /> + </div> + ); + }); + }); + + e.it('ui:debug', async (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug ctx={{}} />); + + /** + * (prior platform-0.0.2) + */ + + // const dev = Dev.tools<T>(e, initial); + // dev.section('Properties', (dev) => { + // dev.boolean((btn) => { + // const value = (state: T) => Boolean(state.props.flipX); + // btn + // .label((e) => `flipX`) + // .value((e) => value(e.state)) + // .onClick((e) => e.change((d) => Dev.toggle(d.props, 'flipX'))); + // }); + // + // dev.boolean((btn) => { + // const value = (state: T) => Boolean(state.props.flipY); + // btn + // .label((e) => `flipY`) + // .value((e) => value(e.state)) + // .onClick((e) => e.change((d) => Dev.toggle(d.props, 'flipY'))); + // }); + // + // dev.boolean((btn) => + // btn + // .label((e) => { + // const offset = e.state.props.offset; + // return `offset: ${offset ? `[${offset.toString()}]` : '<none>'}`; + // }) + // .value((e) => Boolean(e.state.props.offset)) + // .onClick((e) => { + // e.change((d) => { + // const exists = Boolean(d.props.offset); + // d.props.offset = exists ? undefined : [35, -40]; + // }); + // }), + // ); + // }); + // + // dev.hr(5, 20); + // + // dev.section('Debug', (dev) => { + // dev.boolean((btn) => + // btn + // .label((e) => `inherit color: ${e.state.debug.inheritColor ? 'magenta' : '(none)'}`) + // .value((e) => e.state.debug.inheritColor) + // .onClick((e) => e.change((d) => Dev.toggle(d.debug, 'inheritColor'))), + // ); + // }); + // + // dev.hr(5, 20); + // + // dev.section('Debug', (dev) => { + // const color = (name: string, value?: string) => { + // dev.button(name, (e) => e.change((d) => (d.props.color = value))); + // }; + // color('black (dark)', COLORS.DARK); + // color('cyan', COLORS.CYAN); + // color('blue', COLORS.BLUE); + // color('red', COLORS.RED); + // dev.hr(-1, 5); + // color('<undefined> - inherit'); + // }); + // + // dev.hr(); + // + // dev.section('Tooltip', (dev) => { + // dev.button('"Hello"', (e) => e.change((d) => (d.props.tooltip = 'Hello'))); + // dev.button('none', (e) => e.change((d) => (d.props.tooltip = undefined))); + // }); + }); + + e.it('ui:footer', async (e) => { + // const dev = Dev.tools<T>(e, initial); + // dev.footer + // .border(-0.1) + // .render<T>((e) => <Dev.Object name={'spec'} data={e.state} expand={1} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/common.ts b/code/sys.ui/ui-react-components/src/ui/Icon/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/m.Icon.tsx b/code/sys.ui/ui-react-components/src/ui/Icon/m.Icon.tsx new file mode 100644 index 0000000000..b8bffbb59f --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/m.Icon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import type { t } from './common.ts'; +import { IconView } from './ui.tsx'; + +/** + * Tools for rendering icons. + */ +export const Icon: t.IconLib = { + renderer: (type) => (props) => <IconView type={type} {...props} />, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/mod.ts b/code/sys.ui/ui-react-components/src/ui/Icon/mod.ts new file mode 100644 index 0000000000..c987d7ef55 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/mod.ts @@ -0,0 +1,9 @@ +/** + * @module + * SVC Icons Components + * + * References: + * - https://react-icons.github.io/react-icons + * - https://fonts.google.com/icons + */ +export { Icon } from './m.Icon.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/t.ts b/code/sys.ui/ui-react-components/src/ui/Icon/t.ts new file mode 100644 index 0000000000..cc3df1d600 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/t.ts @@ -0,0 +1,47 @@ +import type React from 'react'; +import type { IconType } from 'react-icons'; +import type { t } from '../common.ts'; + +type MouseHandler = React.MouseEventHandler; + +/** + * Tools for rendering icons. + */ +export type IconLib = { + /** Factory that produces an icon-renderer for the given Icon definition. */ + renderer(type: IconType): t.IconRenderer; +}; + +/** + * An <Icon> component function. + */ +export type IconRenderer = (props: IconProps) => JSX.Element; + +/** + * <Component>: Display properties for an icon. + */ +export type IconProps = { + size?: number; + color?: number | string; + opacity?: t.Percent; + tooltip?: string; + offset?: [number, number]; // x,y | (+/-) pixels. + flipX?: boolean; + flipY?: boolean; + margin?: t.CssMarginInput; + style?: t.CssInput; + onClick?: MouseHandler; + onDoubleClick?: MouseHandler; + onMouseDown?: MouseHandler; + onMouseUp?: MouseHandler; + onMouseEnter?: MouseHandler; + onMouseLeave?: MouseHandler; +}; + +/** + * <Component>: The inner renderer of an icon. + */ +export type IconViewProps = t.IconProps & { + type: IconType; + tabIndex?: number; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Icon/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Icon/ui.tsx new file mode 100644 index 0000000000..52050b8d68 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icon/ui.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { type t, Color, Style, css } from './common.ts'; + +type P = t.IconViewProps; + +/** + * The renderer of the <SVG> icon. + */ +export const IconView: React.FC<P> = (props) => { + const { size = 24, opacity } = props; + const Component = props.type; + + const styles = { + base: css({ + display: 'inline-block', + overflow: 'hidden', + Size: size, + transform: wrangle.transform(props), + opacity: opacity === undefined ? 1 : opacity, + ...Style.toMargins(props.margin), + }), + }; + + return ( + <div + className={css(styles.base, props.style).class} + tabIndex={props.tabIndex} + title={props.tooltip} + onClick={props.onClick} + onDoubleClick={props.onDoubleClick} + onMouseDown={props.onMouseDown} + onMouseUp={props.onMouseUp} + onMouseEnter={props.onMouseEnter} + onMouseLeave={props.onMouseLeave} + > + <Component size={size} color={wrangle.color(props)} /> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + color(props: P) { + return props.color ? Color.format(props.color) : undefined; + }, + + transform(props: P) { + const { offset, flipX, flipY } = props; + let res = ''; + if (offset) res += ` translate(${offset[0]}px, ${offset[1]}px)`; + if (flipX) res += ` scaleX(-1)`; + if (flipY) res += ` scaleY(-1)`; + return res.trim() || undefined; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Icons.ts b/code/sys.ui/ui-react-components/src/ui/Icons.ts new file mode 100644 index 0000000000..fed6462ca4 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Icons.ts @@ -0,0 +1,14 @@ +import { MdFace, MdErrorOutline } from 'react-icons/md'; +import { VscSymbolClass } from 'react-icons/vsc'; +import { Icon } from './Icon/mod.ts'; + +const icon = Icon.renderer; + +/** + * Icon collection: + */ +export const Icons = { + Face: icon(MdFace), + Object: icon(VscSymbolClass), + Error: icon(MdErrorOutline), +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/-.test.ts b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-.test.ts new file mode 100644 index 0000000000..0dbc06b146 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from '../../-test.ts'; +import { Svg } from './mod.ts'; + +describe('Image.Svg', () => { + it('API', async () => { + const { SvgElement } = await import('./common.ts'); + expect(Svg.Element).to.equal(SvgElement); + }); + + it('import: Sample', async () => { + /** + * NOTE: this will cause an error in Deno, as '.svg' imports are not supported. + * Solutions: + * - Explore using the FileMap. + */ + // const { Sample } = await import('./-SPEC.Sample.tsx'); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.Debug.tsx new file mode 100644 index 0000000000..afeb36f74e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.Debug.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, Color, css, Signal } from './common.ts'; + +/** + * Types: + */ +export type DebugImportStyle = 'Static' | 'Function ā Promise'; +export type DebugImage = 'Small' | 'Larger'; +export type DebugProps = { debug: DebugSignals; style?: t.CssValue }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals: + */ +export function createDebugSignals() { + const s = Signal.create; + const props = { + theme: s<t.CommonTheme>('Light'), + width: s<number | undefined>(350), + color: s<'dark' | 'blue'>('dark'), + importStyle: s<DebugImportStyle>('Function ā Promise'), + image: s<DebugImage>('Small'), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.width.value; + p.image.value; + p.color.value; + p.image.value; + p.importStyle.value; + }, + }; + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<P> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle(p.theme, ['Light', 'Dark'])} + /> + <hr /> + <Button + block + label={`width: ${p.width.value ?? '<undefined>'}`} + onClick={() => Signal.cycle(p.width, [80, 200, 350, undefined])} + /> + <Button + block + label={`import style: ${p.importStyle}`} + onClick={() => { + Signal.cycle<DebugImportStyle>(p.importStyle, ['Static', 'Function ā Promise']); + }} + /> + + <hr /> + + <Button + block={true} + label={`image (src): ${p.image}`} + onClick={() => { + Signal.cycle<DebugImage>(p.image, ['Small', 'Larger']); + }} + /> + + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.Sample.tsx b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.Sample.tsx new file mode 100644 index 0000000000..3fc166cde4 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.Sample.tsx @@ -0,0 +1,117 @@ +import React from 'react'; + +import StaticLarger from '../../-sample/images/sample.larger.svg'; +import StaticSmall from '../../-sample/images/sample.small.svg'; + +import type { DebugImage, DebugImportStyle, DebugSignals } from './-SPEC.Debug.tsx'; +import { type t, css, Signal } from './common.ts'; +import { Svg } from './mod.ts'; + +export type SampleProps = { signals: DebugSignals }; + +/** + * SAMPLE: render an SVG within a React component. + */ +export const Sample: React.FC<SampleProps> = (props) => { + const { signals } = props; + const p = signals.props; + const input = wrangle.importInput(p.importStyle.value, p.image.value); + const width = p.width.value; + const isFill = width === undefined; + + type H = HTMLDivElement; + const viewbox = wrangle.size(p.image.value); + // const svg = Svg.useSvg<H>(input, viewbox); + const svg = Svg.useSvg<H>(input, viewbox, (e) => { + /** + * Optional: configuration. + */ + // e.draw.width(1234) + }); + + console.groupCollapsed(`š³ SVG (hook)`); + console.info(svg); + console.info('input:', input); + console.info(`svg.query('#tick'):`, svg.query('#tick')); + console.info(`svg.queryAll('line'):`, svg.queryAll('line')); + console.groupEnd(); + + /** + * Redraw the component on signal changes. + */ + Signal.useRedrawEffect(() => { + p.width.value; + p.theme.value; + p.color.value; + p.image.value; + p.importStyle.value; + }); + + /** + * Dynamically adjust the SVG on signal changes. + */ + Signal.useEffect(() => { + const width = p.width.value; + const color = p.color.value; + + const draw = svg.draw; + if (!draw) return; + + const tick = draw.findOne('#tick'); + const borderOutline = draw.findOne('#border-outline'); + + if (tick) { + tick.attr({ opacity: color === 'dark' ? 1 : 0.2 }); + } + if (borderOutline) { + borderOutline.attr({ stroke: width === 200 ? '#383057' : '#0000FF' }); + } + }); + + /** + * Render: + */ + const styles = { + base: css({ + position: 'relative', + overflow: 'hidden', + lineHeight: 0, // NB: ensure no "baseline" gap below the <MediaPlayer>. + }), + svg: css({ + Absolute: isFill ? [0, null, null, 0] : undefined, + width: isFill ? '100%' : undefined, + height: isFill ? '100%' : undefined, + }), + }; + + return ( + <div className={styles.base.class}> + <div ref={svg.ref} className={styles.svg.class} /> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + importInput(importStyle: DebugImportStyle, image: DebugImage = 'Small'): t.SvgImportInput { + if (importStyle === 'Static') { + return image === 'Small' ? StaticSmall : StaticLarger; + } + + if (importStyle === 'Function ā Promise') { + return image === 'Small' + ? () => import('../../-sample/images/sample.small.svg') + : () => import('../../-sample/images/sample.larger.svg'); + } + + throw new Error(`Import style "${importStyle}" not supported.`); + }, + + size(image: DebugImage) { + if (image === 'Small') return [1059, 1059]; + if (image === 'Larger') return [233, 98]; + throw new Error(`Image "${image}" not supported.`); + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.tsx new file mode 100644 index 0000000000..6e42a04556 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/-SPEC.tsx @@ -0,0 +1,34 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Sample } from './-SPEC.Sample.tsx'; + +export default Spec.describe('Image.Svg', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + const updateSize = () => { + const width = p.width.value; + if (width === undefined) ctx.subject.size('fill'); + else ctx.subject.size([width, null]); + }; + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + updateSize(); + ctx.redraw(); + }); + + ctx.subject.display('grid').render((e) => { + return <Sample signals={debug} />; + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/common.ts b/code/sys.ui/ui-react-components/src/ui/Image.Svg/common.ts new file mode 100644 index 0000000000..7a5872e88e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/common.ts @@ -0,0 +1,2 @@ +export { SVG, Element as SvgElement } from '@svgdotjs/svg.js'; +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/m.Svg.ts b/code/sys.ui/ui-react-components/src/ui/Image.Svg/m.Svg.ts new file mode 100644 index 0000000000..e862b14de5 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/m.Svg.ts @@ -0,0 +1,14 @@ +import { SvgElement as Element } from './common.ts'; +import { useSvg } from './use.Svg.ts'; + +/** + * Helpers for working with SVG objects within the given container element. + * + * NOTE: + * This helper assumes <svg> data assembled via the webpack plugin + * (see [cell.compiler]). + */ +export const Svg = { + useSvg, + Element, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/mod.ts b/code/sys.ui/ui-react-components/src/ui/Image.Svg/mod.ts new file mode 100644 index 0000000000..9655f8e21e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/mod.ts @@ -0,0 +1,6 @@ +/** + * @module + * SVG Image Renderer (library) + * See: https://svgjs.dev/ + */ +export { Svg } from './m.Svg.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/t.ts b/code/sys.ui/ui-react-components/src/ui/Image.Svg/t.ts new file mode 100644 index 0000000000..79a3d01f93 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/t.ts @@ -0,0 +1,57 @@ +import type { Element as SvgElement } from '@svgdotjs/svg.js'; +import type { t } from './common.ts'; + +export { SvgElement }; + +type NumberWidth = t.Pixels; +type NumberHeight = t.Pixels; + +/** + * SVG rendering tools. + */ +export type SvgLib = { + readonly Element: SvgElement; + readonly useSvg: UseSvgFactory; +}; + +/** + * Hook: SVG image import/renderer. + */ +export type UseSvgFactory = { + <T extends HTMLElement>( + svgImport: SvgImportInput, + viewBox: [NumberWidth, NumberHeight], + init?: UseSvgInit<T> | NumberWidth, + ): SvgInstance<T>; + + <T extends HTMLElement>( + svgImport: SvgImportInput, + viewBox: number[], // NB: type-hack so typescript does not complain when number-arrays (rather than a tuple) is passed. + init?: UseSvgInit<T> | NumberWidth, + ): SvgInstance<T>; +}; + +/** Input parameter for the `useSvg` hook. */ +export type SvgImportInput = string | (() => SvgImportPromise); + +/** An dynamic import of an SVG file: eg, `import('path/to/file.svg')`. */ +export type SvgImportPromise = Promise<{ default: string }>; + +/** Callback to initialize the SVG upon creation. */ +export type UseSvgInit<T extends HTMLElement> = (e: UseSvgInitArgs<T>) => void; +export type UseSvgInitArgs<T extends HTMLElement> = Pick<SvgInstance<T>, 'query' | 'queryAll'> & { + /** Drawing API. */ + readonly draw: SvgElement; +}; + +/** + * An instance of an SVG image with API for manipulating + * the it programatically. + */ +export type SvgInstance<T extends HTMLElement> = { + readonly ready: boolean; + readonly ref: React.RefObject<T>; + readonly draw?: SvgElement; + query(selector: string): SvgElement | undefined; + queryAll(selector: string): SvgElement[]; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Image.Svg/use.Svg.ts b/code/sys.ui/ui-react-components/src/ui/Image.Svg/use.Svg.ts new file mode 100644 index 0000000000..5f4a176d64 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Image.Svg/use.Svg.ts @@ -0,0 +1,115 @@ +import { useEffect, useRef, useState } from 'react'; +import { type t, rx, SVG, SvgElement } from './common.ts'; + +/** + * Hook: SVG image import/renderer. + */ +export function useSvg<T extends HTMLElement>( + svgImport: string | (() => t.SvgImportPromise), + viewBox: [t.Pixels, t.Pixels] | number[], + init?: t.UseSvgInit<T> | number, +): t.SvgInstance<T> { + type R = t.SvgInstance<T>; + + const ref = useRef<T>(null); + const drawRef = useRef<SvgElement>(); + + const [ready, setReady] = useState(false); + const [importString, setImportString] = useState(''); + const [svgMarkup, setSvgMarkup] = useState(''); + + /** + * Methods: + */ + const query: R['query'] = (selector) => { + if (!ref.current) return undefined; + const el = ref.current.querySelector(selector); + return el ? SVG(el) : undefined; + }; + + const queryAll: R['queryAll'] = (selector) => { + if (!ref.current) return []; + const matches = ref.current.querySelectorAll(selector); + return Array.from(matches).map((el) => SVG(el)); + }; + + /** + * Effect: import SVG data. + */ + useEffect(() => { + const life = rx.lifecycle(); + if (typeof svgImport === 'string') setImportString(svgImport); + if (typeof svgImport === 'function') svgImport().then((m) => setImportString(m.default)); + return life.dispose; + }, [svgImport]); + + /** + * Effect: load/parse the import string into <SVG> markup. + */ + useEffect(() => { + if (!importString) return; + + // Load from embedded data-uri: + if (importString.startsWith('data:image/')) { + const svg = decodeURIComponent(importString.split(',')[1]); + setSvgMarkup(svg); + return; + } + + // Fetch SVG data from server: + const life = rx.lifecycle(); + fetch(importString).then(async (res) => { + if (life.disposed) return; + setSvgMarkup(res.ok ? await res.text() : ''); + }); + return life.dispose; + }, [importString]); + + /** + * Effect: Load the SVG data and inject into DOM. + */ + useEffect(() => { + if (!ref.current) return; + if (!importString) return; + + // Create an SVG canvas inside the container element. + const draw: SvgElement = SVG().addTo(ref.current); + + // NB: Set the viewBox to ensure proper scaling of the inner content. + // These values will be the "viewBox" attribute on the <svg> root tag of the [.svg] file. + const [width, height] = viewBox; + draw.attr({ + viewBox: `0 0 ${width} ${height}`, + width: '100%', + height: '100%', + preserveAspectRatio: 'xMidYMid', + }); + draw.svg(svgMarkup); + + // Initialize. + drawRef.current = draw; + if (typeof init === 'function') init({ draw, query, queryAll }); + if (typeof init === 'number') draw.width(init); + + // Finish up. + setReady(true); + return () => { + draw.clear(); + draw.remove(); + }; + }, [svgMarkup]); + + /** + * API: + */ + const api: R = { + ready, + ref, + get draw() { + return drawRef.current; + }, + query, + queryAll, + }; + return api; +} diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/ObjectView/-SPEC.Debug.tsx new file mode 100644 index 0000000000..70964150ae --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/-SPEC.Debug.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, css, DEFAULTS as D, Signal } from './common.ts'; + +type P = t.ObjectViewProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +const DATA = { + msg: 'š', + count: 0, + foo: { + yes: true, + list: [1, 2, 3], + fn: () => 'hello', + }, +}; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + fontSize: s<P['fontSize']>(), + name: s<P['name']>('my-name'), + data: s<P['data']>({ ...DATA }), + sortKeys: s<P['sortKeys']>(D.sortKeys), + showNonenumerable: s<t.ObjectViewShow['nonenumerable']>(D.show.nonenumerable), + showRootSummary: s<t.ObjectViewShow['rootSummary']>(D.show.rootSummary), + expandPaths: s<string[] | undefined>(), + }; + const p = props; + const api = { + props, + get show(): t.ObjectViewShow { + const nonenumerable = props.showNonenumerable.value; + const rootSummary = props.showRootSummary.value; + return { nonenumerable, rootSummary }; + }, + listen() { + const p = props; + p.theme.value; + p.fontSize.value; + p.data.value; + p.name.value; + p.showNonenumerable.value; + p.showRootSummary.value; + p.sortKeys.value; + p.expandPaths.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}>{'ObjectView'}</div> + + <Button + block + label={`theme: "${p.theme}"`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`fontSize: ${p.fontSize ?? '<undefined>'}`} + onClick={() => Signal.cycle<P['fontSize']>(p.fontSize, [undefined, 14, 18, 32])} + /> + + <hr /> + <Button block label={`sortKeys: ${p.sortKeys}`} onClick={() => Signal.toggle(p.sortKeys)} /> + + <Button + block + label={`showNonenumerable: ${p.showNonenumerable}`} + onClick={() => Signal.toggle(p.showNonenumerable)} + /> + <Button + block + label={`showRootSummary: ${p.showRootSummary}`} + onClick={() => Signal.toggle(p.showRootSummary)} + /> + + <hr /> + <Button + block + label={`expandPaths: ${p.expandPaths.value || '[ ]'}`} + onClick={() => { + const paths = p.expandPaths.value ?? []; + const next = paths.length === 0 ? ['$', '$.foo'] : undefined; + p.expandPaths.value = next; + }} + /> + + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/ObjectView/-SPEC.tsx new file mode 100644 index 0000000000..3b7bcf73f1 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/-SPEC.tsx @@ -0,0 +1,41 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { ObjectView } from './mod.ts'; + +export default Spec.describe('Obj', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size() + .display('grid') + .render((e) => { + const paths = p.expandPaths.value; + return ( + <ObjectView + theme={p.theme.value} + fontSize={p.fontSize.value} + name={p.name.value} + data={p.data.value} + sortKeys={p.sortKeys.value} + show={debug.show} + expand={{ paths }} + /> + ); + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/common.ts b/code/sys.ui/ui-react-components/src/ui/ObjectView/common.ts new file mode 100644 index 0000000000..ba5b1b10d7 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/common.ts @@ -0,0 +1,14 @@ +import type { t } from './common.ts'; +export * from '../common.ts'; + +/** + * Constants: + */ +const theme: t.CommonTheme = 'Light'; +export const DEFAULTS = { + theme, + font: { size: 12 }, + block: true, + sortKeys: false, + show: { rootSummary: false, nonenumerable: false }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/mod.ts b/code/sys.ui/ui-react-components/src/ui/ObjectView/mod.ts new file mode 100644 index 0000000000..5fab976d20 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { ObjectView } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/t.ts b/code/sys.ui/ui-react-components/src/ui/ObjectView/t.ts new file mode 100644 index 0000000000..099589e00d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/t.ts @@ -0,0 +1,32 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type ObjectViewProps = { + name?: string; + data?: any; + + show?: Partial<ObjectViewShow>; + expand?: number | ObjectViewExpand; + sortKeys?: boolean; + + block?: boolean; + fontSize?: number; + theme?: t.CommonTheme; + margin?: t.CssMarginInput; + style?: t.CssInput; +}; + +/** + * Object show feature flags. + */ +export type ObjectViewShow = { + nonenumerable: boolean; + rootSummary: boolean; +}; + +export type ObjectViewExpand = { + level?: number; + paths?: string[] | boolean; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/ui.Renderer.tsx b/code/sys.ui/ui-react-components/src/ui/ObjectView/ui.Renderer.tsx new file mode 100644 index 0000000000..71a0bb8bfe --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/ui.Renderer.tsx @@ -0,0 +1,33 @@ +import { ObjectLabel, ObjectPreview, ObjectRootLabel } from 'react-inspector'; + +type NodeRenderer = (args: NodeRendererArgs) => JSX.Element; +type NodeRendererArgs = { + depth: number; + name: string; + data: any; + isNonenumerable: boolean; + expanded: boolean; +}; + +export function renderer(props: { rootSummary: boolean }): NodeRenderer { + return (args) => { + const { depth, isNonenumerable, name, expanded } = args; + + if (depth === 0) { + if ( + args.data === null || + args.data === undefined || + typeof args.data === 'boolean' || + typeof args.data === 'number' || + typeof args.data === 'string' + ) { + return <ObjectPreview data={args.data} />; + } + + const data = expanded || !props.rootSummary ? {} : args.data; + return <ObjectRootLabel name={args.name} data={data} />; + } else { + return <ObjectLabel name={name} data={args.data} isNonenumerable={isNonenumerable} />; + } + }; +} diff --git a/code/sys.ui/ui-react-components/src/ui/ObjectView/ui.tsx b/code/sys.ui/ui-react-components/src/ui/ObjectView/ui.tsx new file mode 100644 index 0000000000..6c566eadda --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/ObjectView/ui.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from 'react'; +import { chromeDark, chromeLight, ObjectInspector } from 'react-inspector'; +import { type t, css, DEFAULTS, Style } from './common.ts'; +import { renderer } from './ui.Renderer.tsx'; + +type P = t.ObjectViewProps; +const D = DEFAULTS; + +export const ObjectView: React.FC<P> = (props) => { + const { block = D.block, sortKeys = D.sortKeys } = props; + const { expandLevel, expandPaths } = wrangle.expand(props); + const show = wrangle.show(props); + + const [key, setKey] = useState(wrangle.key(props)); + + /** + * Effect: ensure key/render when the expand level changes. + */ + useEffect(() => setKey(wrangle.key(props)), [expandPaths?.join(), expandLevel]); + + /** + * Render: + */ + const styles = { + base: css({ + display: block ? 'block' : 'inline-block', + ...Style.toMargins(props.margin), + }), + }; + + const theme = wrangle.theme(props); + const el = ( + <ObjectInspector + key={key} + data={props.data} + name={props.name} + theme={theme as any} + sortObjectKeys={sortKeys} + showNonenumerable={show?.nonenumerable} + nodeRenderer={renderer({ rootSummary: show?.rootSummary })} + expandLevel={expandLevel} + expandPaths={expandPaths} + /> + ); + return <div className={css(styles.base, props.style).class}>{el}</div>; +}; + +/** + * Helpers + */ +const wrangle = { + key(props: t.ObjectViewProps) { + const { expandLevel, expandPaths } = wrangle.expand(props); + return `obj:${expandLevel ?? 0}:${(expandPaths ?? []).join(',')}`; + }, + + theme(props: t.ObjectViewProps) { + const fontSize = `${props.fontSize ?? DEFAULTS.font.size}px`; + const lineHeight = '1.5em'; + return { + ...wrangle.baseTheme(props.theme), + BASE_BACKGROUND_COLOR: 'transparent', + BASE_FONT_SIZE: fontSize, + TREENODE_FONT_SIZE: fontSize, + BASE_LINE_HEIGHT: lineHeight, + TREENODE_LINE_HEIGHT: lineHeight, + }; + }, + + baseTheme(theme?: t.CommonTheme) { + theme = theme ?? DEFAULTS.theme; + if (theme === 'Light') return chromeLight; + if (theme === 'Dark') return chromeDark; + throw new Error(`Theme '${theme}' not supported.`); + }, + + show(props: P): t.ObjectViewShow { + const D = DEFAULTS.show; + const { show = {} } = props; + const { nonenumerable = D.nonenumerable, rootSummary = D.nonenumerable } = show; + return { nonenumerable, rootSummary }; + }, + + expand(props: P) { + const { expand } = props; + let expandLevel: number | undefined = undefined; + let expandPaths: string[] | undefined; + + if (typeof expand === 'number') { + expandLevel = expand; + } + + if (typeof expand === 'object') { + expandLevel = expand.level; + expandPaths = Array.isArray(expand.paths) ? expand.paths : undefined; + } + + return { expandLevel, expandPaths }; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Panel/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Panel/-SPEC.Debug.tsx new file mode 100644 index 0000000000..b266f8668d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Panel/-SPEC.Debug.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, Color, css, Signal } from './common.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssValue }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals: + */ +export function createDebugSignals() { + const s = Signal.create; + const props = { theme: s<t.CommonTheme>('Light') }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + }, + }; + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<P> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => { + p.theme.value; + }); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Panel/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Panel/-SPEC.tsx new file mode 100644 index 0000000000..9cc3a0254b --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Panel/-SPEC.tsx @@ -0,0 +1,23 @@ +import { Dev, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Panel } from './mod.ts'; + +export default Spec.describe('Panel', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + Dev.Theme.signalEffect(ctx, p.theme, 1); + + ctx.subject + .size([224, null]) + .display('grid') + .render((e) => <Panel theme={p.theme.value} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Panel/common.ts b/code/sys.ui/ui-react-components/src/ui/Panel/common.ts new file mode 100644 index 0000000000..fc72b6f116 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Panel/common.ts @@ -0,0 +1,2 @@ +export * from '../common.ts'; +export const DEFAULTS = {} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Panel/mod.ts b/code/sys.ui/ui-react-components/src/ui/Panel/mod.ts new file mode 100644 index 0000000000..e42bc11c79 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Panel/mod.ts @@ -0,0 +1 @@ +export { Panel } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Panel/t.ts b/code/sys.ui/ui-react-components/src/ui/Panel/t.ts new file mode 100644 index 0000000000..a7094b27c2 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Panel/t.ts @@ -0,0 +1,11 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type PanelProps = { + text?: string; + debug?: boolean; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Panel/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Panel/ui.tsx new file mode 100644 index 0000000000..13857aee14 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Panel/ui.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { type t, Color, css } from './common.ts'; + +export const Panel: React.FC<t.PanelProps> = (props) => { + const { text = 'š· Panel' } = props; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + backgroundColor: 'rgba(255, 0, 0, 0.1)' /* RED */, + color: theme.fg, + padding: 3, + borderRadius: 5, + fontSize: 14, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div>{text}</div> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Concept/-SPEC.Debug.tsx new file mode 100644 index 0000000000..0577faaf76 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/-SPEC.Debug.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { Color, css, Signal, type t } from './common.ts'; + +export type DebugProps = { + ctx: { signals: t.VideoPlayerSignals }; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = DebugProps; + +/** + * Component + */ +export const Debug: React.FC<P> = (props) => { + const { ctx } = props; + const s = ctx.signals; + const p = s.props; + + Signal.useRedrawEffect(() => { + p.ready.value; + p.loop.value; + p.playing.value; + }); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ color: theme.fg }), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title).class}>{'WIP'}</div> + + <Button block={true} label={`action: jumpTo(12, play)`} onClick={() => s.jumpTo(12)} /> + <Button + block={true} + label={`action: jumpTo(12, paused)`} + onClick={() => s.jumpTo(12, { play: false })} + /> + <hr /> + <Button block={true} label={`play: ${p.playing}`} onClick={() => toggle(p.playing)} /> + <Button block={true} label={`loop: ${p.loop}`} onClick={() => toggle(p.loop)} /> + </div> + ); +}; + +/** + * Helpers + */ +const toggle = (signal: t.Signal<boolean>) => (signal.value = !signal.value); diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Concept/-SPEC.tsx new file mode 100644 index 0000000000..e174d07198 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/-SPEC.tsx @@ -0,0 +1,28 @@ +import { Spec, expect } from '../-test.ui.ts'; +import { Debug } from './-SPEC.Debug.tsx'; + +import { Player } from '../../mod.ts'; +import { sampleTimestamps } from '../Player.Thumbnails/-SPEC.sample.ts'; +import { ConceptPlayer } from './mod.ts'; + +export default Spec.describe('VideoPlayer', (e) => { + const s = Player.Video.signals(); + + e.it('API', (e) => { + expect(Player.Concept.View).to.equal(ConceptPlayer); + }); + + e.it('init', async (e) => { + const ctx = Spec.ctx(e); + ctx.subject.size([688, null]).render((e) => { + return ( + <Player.Concept.View thumbnails={true} timestamps={sampleTimestamps} videoSignals={s} /> + ); + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug ctx={{ signals: s }} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/-tmp.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Concept/-tmp.tsx new file mode 100644 index 0000000000..ca83640e0a --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/-tmp.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { type t, rx, Color, css, Button } from './common.ts'; + +export type OverlayProps = { + ctx: t.ConceptPlayerProps; + theme?: t.CommonTheme; + style?: t.CssInput; + onClose: () => void; +}; + +type P = OverlayProps; + +/** + * Component. + */ +export const Overlay: React.FC<P> = (props) => { + const {} = props; + const [el, setEl] = useState<React.ReactElement>(); + + useEffect(() => { + const life = rx.disposable(); + import('./ui.tsx').then(({ ConceptPlayer }) => { + setEl(<ConceptPlayer {...props.ctx} />); + }); + return life.dispose; + }, []); + + /** + * Render:. + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + Absolute: 0, + position: 'fixed', + backgroundColor: theme.bg, + color: theme.fg, + zIndex: 9999999, + }), + body: css({ Absolute: 20 }), + close: css({ + Absolute: [null, 10, 10, null], + }), + }; + + const elBody = ( + <div className={css(styles.base, props.style).class}> + <div className={styles.body.class}>{el}</div> + <Button label={'Close'} onClick={props.onClose} style={styles.close} /> + </div> + ); + + return createPortal(elBody, document.body); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/common.ts b/code/sys.ui/ui-react-components/src/ui/Player.Concept/common.ts new file mode 100644 index 0000000000..d197b80a38 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/common.ts @@ -0,0 +1,8 @@ +export * from '../common.ts'; + +export { Button } from '../Button/mod.ts'; +export { Icons } from '../Icons.ts'; +export { Timestamp, Thumbnails } from '../Player.Thumbnails/mod.ts'; +export { playerSignalsFactory, VideoPlayer } from '../Player.Video/mod.ts'; + +export const DEFAULTS = {} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/mod.ts b/code/sys.ui/ui-react-components/src/ui/Player.Concept/mod.ts new file mode 100644 index 0000000000..484a1b19aa --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/mod.ts @@ -0,0 +1 @@ +export { ConceptPlayer } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/t.ts b/code/sys.ui/ui-react-components/src/ui/Player.Concept/t.ts new file mode 100644 index 0000000000..efb6cdf51e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/t.ts @@ -0,0 +1,17 @@ +import type { t } from './common.ts'; + +/** + * An extended video player. + */ +export type ConceptPlayerProps = { + debug?: boolean; + title?: string; + video?: string; + timestamps?: t.VideoTimestamps; + thumbnails?: boolean; + + theme?: t.CommonTheme; + style?: t.CssInput; + + videoSignals?: t.VideoPlayerSignals; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/ui.DisplayImage.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Concept/ui.DisplayImage.tsx new file mode 100644 index 0000000000..acca76a74d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/ui.DisplayImage.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { type t, Color, css, Icons, Signal, Time, Timestamp } from './common.ts'; + +export type DisplayImageProps = { + timestamps: t.VideoTimestamps; + theme?: t.CommonTheme; + style?: t.CssInput; + videoSignals: t.VideoPlayerSignals; +}; + +type P = DisplayImageProps; + +/** + * Component + */ +export const DisplayImage: React.FC<P> = (props) => { + const { videoSignals: s, timestamps } = props; + + const [error, setError] = useState(false); + const [src, setSrc] = useState<string>(); + const updateSrc = (value?: string) => { + setSrc(value); + setError(false); + }; + + /** + * Lifecycle + */ + Signal.useEffect(() => { + const elapsed = s.props.currentTime.value; + const match = Timestamp.find(timestamps, elapsed); + if (match?.image !== src) Time.delay(() => updateSrc(match?.image)); + }); + + useEffect(() => { + if (src) { + const image = new Image(); + image.src = src ?? ''; + image.onerror = () => setError(true); + } + }, [src]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ position: 'relative', color: theme.fg }), + image: css({ + Absolute: 10, + backgroundImage: src ? `url(${src})` : undefined, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }), + error: css({ Absolute: 0, display: 'grid', placeItems: 'center' }), + }; + + const elError = error && ( + <div className={styles.error.class}> + <Icons.Error color={Color.RED} /> + </div> + ); + + const tooltip = !error ? '' : `Failed to load image: ${src}`; + + return ( + <div className={css(styles.base, props.style).class} title={tooltip}> + {src && <div className={styles.image.class} />} + {elError} + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Concept/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Concept/ui.tsx new file mode 100644 index 0000000000..d6c3031f73 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Concept/ui.tsx @@ -0,0 +1,104 @@ +import React, { useRef, useState } from 'react'; +import { Overlay } from './-tmp.tsx'; +import { + type t, + Button, + Color, + css, + playerSignalsFactory, + Thumbnails, + VideoPlayer, +} from './common.ts'; +import { DisplayImage } from './ui.DisplayImage.tsx'; + +type P = t.ConceptPlayerProps; + +/** + * Component + */ +export const ConceptPlayer: React.FC<P> = (props) => { + const timestamps = wrangle.timestamps(props); + const playerSignalsRef = wrangle.playerSignals(props); + const playerSignals = playerSignalsRef.current; + + const [showOverlay, setShowOverlay] = useState<boolean>(false); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ color: theme.fg, fontSize: 14 }), + body: css({ display: 'grid', gridTemplateColumns: `1fr 2fr`, columnGap: '5px' }), + videoPlayer: css({}), + thumbnails: css({ marginTop: 30 }), + + tmp: css({ + marginTop: 50, + }), + }; + + const elThumbnails = props.thumbnails && ( + <Thumbnails + style={styles.thumbnails} + timestamps={timestamps} + videoSignals={props.videoSignals} + onTimestampClick={(e) => { + const isPlaying = playerSignals.props.playing.value; + const target = e.total.sec; + if (e.isCurrent) { + playerSignals.toggle(!isPlaying); + } else { + playerSignals.jumpTo(target); + } + }} + /> + ); + + const elPlayer = ( + <VideoPlayer title={props.title} style={styles.videoPlayer} signals={playerSignals} /> + ); + + const elTmp = ( + <div className={styles.tmp.class}> + <Button + label={'š· Full Screen Concept Player'} + onClick={() => { + /** + * TODO š· + */ + setShowOverlay(true); + }} + /> + </div> + ); + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.body.class}> + {elPlayer} + <DisplayImage timestamps={timestamps} videoSignals={playerSignals} /> + </div> + {elThumbnails} + + {elTmp} + {showOverlay && <Overlay ctx={props} onClose={() => setShowOverlay(false)} />} + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + timestamps(props: P) { + const { timestamps } = props; + if (typeof timestamps !== 'object') return {}; + return timestamps; + }, + + playerSignals(props: P) { + type T = t.VideoPlayerSignals; + return useRef<T>(props.videoSignals ?? playerSignalsFactory()); + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-.test.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-.test.ts new file mode 100644 index 0000000000..98c336bc85 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-.test.ts @@ -0,0 +1,19 @@ +import { type t, expect, describe, it } from '../../-test.ts'; +import { Timestamp } from './mod.ts'; + +describe('Thumbnails', () => { + describe('Video Timestamps', () => { + it('has image', () => { + const timestamps: t.VideoTimestamps = { + '00:00:00.000': {}, + '00:00:10.000': { image: '/path/to-image.png' }, + }; + + const a = Timestamp.find(timestamps, 1); + const b = Timestamp.find(timestamps, 12); + + expect(a?.image).to.eql(undefined); + expect(b?.image).to.eql('/path/to-image.png'); + }); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-SPEC.sample.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-SPEC.sample.ts new file mode 100644 index 0000000000..2f0c94a019 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-SPEC.sample.ts @@ -0,0 +1,19 @@ +import { type t } from './common.ts'; + +const IMG = { + KITTEN: `https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.wallpaperflare.com%2Fstatic%2F732%2F902%2F566%2Fanimal-pet-cute-grey-wallpaper.jpg&f=1&nofb=1&ipt=a69e2c104f05537e10d73517716481f2b1db71244a10fdb108b07fd6e9a01652&ipo=images`, + LIZARD: `https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Ftse1.mm.bing.net%2Fth%3Fid%3DOIP.uZCiSQquNq1uSARFRpKM8wHaEK%26pid%3DApi&f=1&ipt=ecb28ebe255e465723a38e03dd84a17cbbba3f734802b7ad12266035fabaa71d&ipo=images`, + NOT_FOUND: '/404.png', +} as const; + +export const sampleTimestamps: t.VideoTimestamps = { + '00:00:01.000': { image: IMG.KITTEN }, + '00:00:03.123': { image: IMG.LIZARD }, + '00:00:6.001': { image: IMG.NOT_FOUND }, + '00:01:38.002': { image: IMG.NOT_FOUND }, + '00:01:60.003': { image: IMG.NOT_FOUND }, + '00:01:68.004': { image: IMG.NOT_FOUND }, + '00:02:23.000': { image: IMG.NOT_FOUND }, + '00:03:01.001': { image: IMG.LIZARD }, + '00:01:05.002': { image: IMG.KITTEN }, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-SPEC.tsx new file mode 100644 index 0000000000..729a92b120 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/-SPEC.tsx @@ -0,0 +1,26 @@ +import { Spec, expect } from '../-test.ui.ts'; +import { Player } from '../../mod.ts'; + +import { sampleTimestamps } from './-SPEC.sample.ts'; +import { Thumbnails } from './mod.ts'; + +export default Spec.describe('VideoPlayer', (e) => { + const s = Player.Video.signals(); + + e.it('API', (e) => { + expect(Player.Timestamp.Thumbnails.View).to.equal(Thumbnails); + }); + + e.it('init', async (e) => { + const ctx = Spec.ctx(e); + ctx.subject.size([520, null]).render((e) => { + return ( + <Player.Timestamp.Thumbnails.View + timestamps={sampleTimestamps} + onTimestampClick={(e) => console.info(`ā”ļø onTimestampClick:`, e)} + videoSignals={s} + /> + ); + }); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/common.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/common.ts new file mode 100644 index 0000000000..1822a5fd88 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/common.ts @@ -0,0 +1,7 @@ +export * from '../common.ts'; +export { Icons } from '../Icons.ts'; +import { PlayerColors } from '../Player/common.ts'; + +export const DEFAULTS = { + BLUE: PlayerColors.BLUE, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/mod.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/mod.ts new file mode 100644 index 0000000000..a893b68698 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/mod.ts @@ -0,0 +1,6 @@ +/** + * @module + * Timestamp thumbnails edit controls. + */ +export { Timestamp } from './u.ts'; +export { Thumbnails } from './ui.Thumbnails.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/t.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/t.ts new file mode 100644 index 0000000000..79e914d320 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/t.ts @@ -0,0 +1,27 @@ +import type { t } from './common.ts'; + +/** + * <Component> + */ +export type ThumbnailsProps = { + timestamps?: t.VideoTimestamps; + + theme?: t.CommonTheme; + style?: t.CssInput; + + videoSignals?: t.VideoPlayerSignals; + onTimestampClick?: t.VideoTimestampHandler; +}; + +/** + * Timestamps. + */ +export type VideoTimestamps = t.Timestamps<t.VideoTimestampData>; +export type VideoTimestamp = t.Timestamp<t.VideoTimestampData>; +export type VideoTimestampData = { image?: t.StringPath }; + +/** + * Events. + */ +export type VideoTimestampHandler = (e: VideoTimestampHandlerArgs) => void; +export type VideoTimestampHandlerArgs = t.VideoTimestamp & { isCurrent: boolean }; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/u.timestamp.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/u.timestamp.ts new file mode 100644 index 0000000000..77c80b9413 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/u.timestamp.ts @@ -0,0 +1,57 @@ +import { type t, Timestamp as Std } from './common.ts'; + +const round = 3; +const unit = 'secs'; + +/** + * Check if a given timestamp is the current one based on the elapsed time. + */ +function isCurrent( + timestamps: t.VideoTimestamps, + timestamp: t.StringTimestamp, + currentTime: t.Secs, +): boolean { + return Std.isCurrent(timestamps, timestamp, currentTime, { unit, round }); +} + +/** + * Lookup a timestamp given the current elapsed time. + */ +function find(timestamps: t.VideoTimestamps, elapsed: t.Secs): t.VideoTimestampData | undefined { + return Std.find(timestamps, elapsed, { unit, round })?.data; +} + +/** + * Convert the set of { "HH:MM:SS:mmm":<value> } timestamp + * strings into list of sorted stuctures. + */ +function parseTimes(timestamps?: t.VideoTimestamps): t.VideoTimestamp[] { + return Std.parse<t.VideoTimestampData>(timestamps, { round }); +} + +/** + * Parse a "HH:MM:DD:mmm" string. + */ +function parseTime(timestamp: string): t.TimeDuration { + return Std.parse(timestamp, { round }); +} + +/** + * Generate a sub-range for a timestamp within a map of timestamps. + */ +function range(timestamps: t.VideoTimestamps | undefined, location: t.Secs | t.StringTimestamp) { + if (!timestamps) return undefined; + return Std.range(timestamps, location, { unit, round }); +} + +export const Timestamp = { + parseTime, + parseTimes, + isCurrent, + find, + range, + toString(input: t.VideoTimestamp | string) { + const d = typeof input === 'string' ? input : input.total; + return Std.toString(d); + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/u.ts b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/u.ts new file mode 100644 index 0000000000..d5f85b49c2 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/u.ts @@ -0,0 +1 @@ +export * from './u.timestamp.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnail.FooterBar.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnail.FooterBar.tsx new file mode 100644 index 0000000000..85b19e3081 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnail.FooterBar.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { type t, Color, css, Signal, DEFAULTS, rx } from './common.ts'; +import { Timestamp } from './u.ts'; + +export type FooterBarProps = { + timestamp: t.StringTimestamp; + timestamps?: t.VideoTimestamps; + isCurrent?: boolean; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = FooterBarProps; + +/** + * Component. + */ +export const FooterBar: React.FC<P> = (props) => { + const { isCurrent, timestamp, timestamps } = props; + const time = wrangle.time(timestamp); + + /** + * Render:. + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + borderTop: `solid 1px ${Color.alpha(theme.fg, 0.1)}`, + fontSize: 12, + PaddingY: 1, + display: 'grid', + placeItems: 'center', + backgroundColor: isCurrent ? DEFAULTS.BLUE : undefined, + color: isCurrent ? Color.WHITE : undefined, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div>{time}</div> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + time(ts: t.StringTimestamp) { + ts = Timestamp.toString(ts); + return ts.slice(0, ts.indexOf('.')); + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnail.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnail.tsx new file mode 100644 index 0000000000..a382ee901e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnail.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { type t, Color, css, Icons, Signal, Time } from './common.ts'; +import { Timestamp } from './u.ts'; +import { FooterBar } from './ui.Thumbnail.FooterBar.tsx'; + +export type ThumbnailProps = { + timestamp: t.StringTimestamp; + timestamps?: t.VideoTimestamps; + data: t.VideoTimestampData; + theme?: t.CommonTheme; + style?: t.CssInput; + onClick?: t.VideoTimestampHandler; + videoSignals?: t.VideoPlayerSignals; +}; + +export const Thumbnail: React.FC<ThumbnailProps> = (props) => { + const { timestamp, timestamps, data, onClick, videoSignals } = props; + const src = data.image; + + const rangeRef = useRef(Timestamp.range(timestamps, timestamp)); + const range = rangeRef.current; + + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(false); + const [isOver, setOver] = useState(false); + const [isDown, setDown] = useState(false); + const [isCurrent, setIsCurrent] = useState(false); + const over = (isOver: boolean) => () => setOver(isOver); + const down = (isDown: boolean) => () => setDown(isDown); + + /** + * Lifecycle. + */ + useEffect(() => { + const image = new Image(); + image.src = src ?? ''; + image.onload = () => setLoaded(true); + image.onerror = () => setError(true); + }, [src]); + + /** + * Highlight when current thumbnail/timestamp. + */ + Signal.useEffect(() => { + const currentTime = videoSignals?.props.currentTime.value ?? -1; + const update = (isCurrent: boolean) => { + // NB: state updated after a micro-delay to ensure writing happens on the next render frame. + Time.delay(() => setIsCurrent(isCurrent)); + }; + + if (timestamps) { + const isCurrent = Timestamp.isCurrent(timestamps, timestamp, currentTime); + update(isCurrent); + } else { + update(false); + } + }); + + /** + * Handlers. + */ + function handleClick() { + const total = Timestamp.parseTime(timestamp); + props.onClick?.({ timestamp, data, total, isCurrent }); + } + + /** + * Render:. + */ + const theme = Color.theme(props.theme); + const borderRadius = 6; + const styles = { + base: css({ + position: 'relative', + userSelect: 'none', + backgroundColor: Color.alpha(theme.fg, 0.03), + color: theme.fg, + width: 100, + height: 80, + display: 'grid', + gridTemplateRows: `1fr auto`, + borderRadius, + border: `solid 1px ${Color.alpha(theme.fg, isOver && onClick ? 0.15 : 0.1)}`, + boxShadow: + isOver && onClick + ? `0 1px ${isDown ? 4 : 15}px 0 ${Color.format(isDown ? -0.05 : -0.1)}` + : undefined, + transform: `translateY(${isDown ? 0 : -1}px)`, + }), + image: { + base: css({ + position: 'relative', + backgroundColor: theme.bg, + borderRadius: `${borderRadius - 1}px ${borderRadius - 1}px 0 0`, + overflow: 'hidden', + display: 'grid', + placeItems: 'center', + }), + img: css({ + Absolute: 0, + backgroundImage: loaded ? `url(${src})` : undefined, + display: 'grid', + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }), + }, + }; + + const tooltip = !error ? '' : `Failed to load image: ${src}`; + const elBody = ( + <div className={styles.image.base.class} title={tooltip}> + {loaded && <div className={styles.image.img.class} />} + {error && <Icons.Error color={Color.RED} />} + </div> + ); + + const elFooter = ( + <FooterBar + timestamp={timestamp} + timestamps={timestamps} + isCurrent={isCurrent} + theme={theme.name} + /> + ); + + return ( + <div + className={css(styles.base, props.style).class} + onMouseEnter={over(true)} + onMouseLeave={over(false)} + onMouseDown={down(true)} + onMouseUp={down(false)} + onClick={handleClick} + > + {elBody} + {elFooter} + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnails.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnails.tsx new file mode 100644 index 0000000000..89b2142f1f --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Thumbnails/ui.Thumbnails.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { type t, Color, css } from './common.ts'; +import { Timestamp } from './u.ts'; +import { Thumbnail } from './ui.Thumbnail.tsx'; + +type P = t.ThumbnailsProps; +export const Thumbnails: React.FC<P> = (props) => { + const { timestamps = {} } = props; + const times = Timestamp.parseTimes(timestamps); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + color: theme.fg, + display: 'flex', + alignContent: 'flex-start', + flexWrap: 'wrap', + gap: '8px', + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + {times.map(({ timestamp, data }, i) => ( + <Thumbnail + key={`${i}.${timestamp}`} + data={data} + timestamp={timestamp} + timestamps={timestamps} + onClick={(e) => props.onTimestampClick?.(e)} + videoSignals={props.videoSignals} + theme={theme.name} + /> + ))} + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Video/-SPEC.Debug.tsx new file mode 100644 index 0000000000..4c71034ff3 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/-SPEC.Debug.tsx @@ -0,0 +1,169 @@ +import React from 'react'; +import { Player } from '../../mod.ts'; +import { Button } from '../Button/mod.ts'; +import { type t, css, D, Signal } from './common.ts'; + +/** + * Types: + */ +export type DebugProps = { + debug: DebugSignals; + theme?: t.CommonTheme; + style?: t.CssInput; +}; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals() { + const s = Signal.create; + + const video = Player.Video.signals({ + // loop: true, + // autoPlay: true, + // showControls: false, + }); + + const props = {}; + const api = { + props, + video, + listen() { + const p = video.props; + p.ready.value; + + // Media: + p.src.value; + p.playing.value; + p.muted.value; + p.autoPlay.value; + p.loop.value; + + // Appearance: + p.showControls.value; + p.showFullscreenButton.value; + p.showVolumeControl.value; + p.background.value; + p.cornerRadius.value; + p.aspectRatio.value; + p.scale.value; + + // Commands: + p.jumpTo.value; + }, + }; + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const video = debug.video; + const p = video.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'Player.Video'}</div> + <div /> + <CurrentTime video={video} /> + </div> + + <Button block label={`method: jumpTo(12, play)`} onClick={() => video.jumpTo(12)} /> + <Button + block + label={`method: jumpTo(12, paused)`} + onClick={() => video.jumpTo(12, { play: false })} + /> + <hr /> + <Button block label={`playing: ${p.playing}`} onClick={() => Signal.toggle(p.playing)} /> + <Button block label={`muted: ${p.muted}`} onClick={() => Signal.toggle(p.muted)} /> + <Button block label={`autoplay: ${p.autoPlay}`} onClick={() => Signal.toggle(p.autoPlay)} /> + <Button block label={`loop: ${p.loop}`} onClick={() => Signal.toggle(p.loop)} /> + <Button + block + label={`background: ${p.background} ā ${p.background.value ? 'fill' : 'fixed-size'}`} + onClick={() => Signal.toggle(p.background)} + /> + <hr /> + <Button + block + label={`showControls: ${p.showControls}`} + onClick={() => Signal.toggle(p.showControls)} + /> + <Button + block + label={`showFullscreenButton: ${p.showFullscreenButton}`} + onClick={() => Signal.toggle(p.showFullscreenButton)} + /> + <Button + block + label={`showVolumeControl: ${p.showVolumeControl}`} + onClick={() => Signal.toggle(p.showVolumeControl)} + /> + <Button + block + label={`cornerRadius: ${p.cornerRadius}`} + onClick={() => Signal.cycle(p.cornerRadius, [0, 5, 10, 15])} + /> + <Button + block + label={`aspectRatio: ${p.aspectRatio}`} + onClick={() => Signal.cycle(p.aspectRatio, [D.aspectRatio, '4/3', '2.39/1', '1/1'])} + /> + <Button + block + label={() => { + const current = p.scale.value; + return `scale: ${typeof current === 'function' ? 'Ęn' : current}`; + }} + onClick={() => { + const fn: t.VideoPlayerScale = (e) => { + const pixels = 1; + const res = e.enlargeBy(pixels); + console.info(`ā”ļø scale (callback):`, e); + console.info(` increment (${pixels}px):`, res); + return res; + }; + Signal.cycle(p.scale, [undefined, 1, fn, 2]); + }} + /> + + <hr /> + + <Button + block + label={`src: ${p.src}`} + onClick={() => + Signal.cycle(p.src, [ + D.video, // Default: "tubes" + 'vimeo/727951677', // Rowan: "group scale" + ]) + } + /> + + <hr /> + </div> + ); +}; + +function CurrentTime(props: { video: t.VideoPlayerSignals; prefix?: string }) { + const { video, prefix = 'elapsed' } = props; + const p = video.props; + Signal.useRedrawEffect(() => p.currentTime.value); + return <div>{`(${prefix}: ${p.currentTime.value.toFixed(2)}s)`}</div>; +} diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Video/-SPEC.tsx new file mode 100644 index 0000000000..8508cdec27 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/-SPEC.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { Signal, Spec, expect } from '../-test.ui.ts'; +import { Player } from '../../mod.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { VideoPlayer } from './mod.ts'; + +export default Spec.describe('VideoPlayer', (e) => { + const debug = createDebugSignals(); + const video = debug.video; + + e.it('API', (e) => { + expect(Player.Video.View).to.equal(VideoPlayer); + }); + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + const updateSize = () => { + const fill = video.props.background.value; + if (fill) ctx.subject.size('fill'); + else ctx.subject.size([520, null]); + }; + + Signal.effect(() => { + updateSize(); + debug.listen(); + }); + updateSize(); + + ctx.subject.display('grid').render((e) => { + return <VideoPlayer signals={video} />; + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/-Signals.test.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/-Signals.test.ts new file mode 100644 index 0000000000..b79a707882 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/-Signals.test.ts @@ -0,0 +1,125 @@ +import { type t, describe, expect, it } from '../../-test.ts'; +import { Player } from '../Player/mod.ts'; +import { D } from './common.ts'; +import { playerSignalsFactory } from './mod.ts'; + +describe('VideoPlayer: Signals API', () => { + describe('props', () => { + it('initial values (defaults)', () => { + const s = playerSignalsFactory(); + const p = s.props; + + expect(p.ready.value).to.eql(false); + expect(p.src.value).to.eql(D.video); + + expect(p.playing.value).to.eql(false); + expect(p.currentTime.value).to.eql(0); + expect(p.loop.value).to.eql(D.loop); + expect(p.autoPlay.value).to.eql(D.autoPlay); + expect(p.muted.value).to.eql(D.muted); + + expect(p.showControls.value).to.eql(true); + expect(p.showFullscreenButton.value).to.eql(D.showFullscreenButton); + expect(p.showVolumeControl.value).to.eql(D.showVolumeControl); + expect(p.cornerRadius.value).to.eql(D.cornerRadius); + expect(p.aspectRatio.value).to.eql(D.aspectRatio); + expect(p.background.value).to.eql(D.background); + expect(p.scale.value).to.eql(D.scale); + + expect(p.jumpTo.value).to.eql(undefined); + + p.playing.value = true; + expect(p.playing.value).to.eql(true); + }); + + it('param: custom { defaults }', () => { + const scale: t.VideoPlayerScale = (e) => e.enlargeBy(1); + const s = Player.Video.signals({ + src: 'vimeo/foobar', + loop: true, + showControls: false, + showFullscreenButton: true, + showVolumeControl: false, + cornerRadius: 0, + aspectRatio: '2.39/1', + autoPlay: true, + muted: true, + background: true, + scale, + }); + + const p = s.props; + expect(p.src.value).to.eql('vimeo/foobar'); + expect(p.loop.value).to.eql(true); + expect(p.aspectRatio.value).to.eql('2.39/1'); + expect(p.autoPlay.value).to.eql(true); + expect(p.muted.value).to.eql(true); + + expect(p.showControls.value).to.eql(false); + expect(p.showFullscreenButton.value).to.eql(true); + expect(p.showVolumeControl.value).to.eql(false); + expect(p.cornerRadius.value).to.eql(0); + expect(p.background.value).to.eql(true); + expect(p.scale.value).to.equal(scale); + }); + + it('param: src param (string)', () => { + const s = Player.Video.signals('vimeo/foobar'); + expect(s.props.src.value).to.eql('vimeo/foobar'); + }); + }); + + describe('methods', () => { + it('jumpTo() method ā props.jumpTo', () => { + const s = playerSignalsFactory(); + expect(s.props.jumpTo.value).to.eql(undefined); + + const res = s.jumpTo(10); + expect(res).to.equal(s); + expect(s.props.jumpTo.value).to.eql({ second: 10, play: true }); + + s.jumpTo(15, { play: false }); + expect(s.props.jumpTo.value).to.eql({ second: 15, play: false }); + }); + + it('play', () => { + const s = playerSignalsFactory(); + const assertPlaying = (value: boolean) => expect(s.props.playing.value).to.eql(value); + assertPlaying(false); + + const res = s.play(); + expect(res).to.equal(s); + assertPlaying(true); + }); + + it('pause', () => { + const s = playerSignalsFactory(); + const assertPlaying = (value: boolean) => expect(s.props.playing.value).to.eql(value); + assertPlaying(false); + + s.play(); + assertPlaying(true); + + const res = s.play().pause(); + expect(res).to.equal(s); + assertPlaying(false); + }); + + it('toggle', () => { + const s = playerSignalsFactory(); + const assertPlaying = (value: boolean) => expect(s.props.playing.value).to.eql(value); + assertPlaying(false); + + const res = s.toggle(); + expect(res).to.equal(s); + assertPlaying(true); + + s.toggle(); + assertPlaying(false); + s.toggle(false); + assertPlaying(false); + s.toggle(true); + assertPlaying(true); + }); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/common.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/common.ts new file mode 100644 index 0000000000..def6418293 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/common.ts @@ -0,0 +1,20 @@ +export * from '../common.ts'; + +/** + * Constants + */ +export const DEFAULTS = { + video: 'vimeo/499921561', // Tubes. + + loop: false, + showFullscreenButton: false, + showVolumeControl: true, + showControls: true, + cornerRadius: 0, + aspectRatio: '16/9', + autoPlay: false, + muted: false, + background: false, + scale: 1, +} as const; +export const D = DEFAULTS; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/m.Signals.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/m.Signals.ts new file mode 100644 index 0000000000..56639151b6 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/m.Signals.ts @@ -0,0 +1,74 @@ +import { type t, DEFAULTS, Signal } from './common.ts'; + +type T = t.VideoPlayerSignals; +type P = t.VideoPlayerSignalProps; +const D = DEFAULTS; + +/** + * Factory: create a new instance of signals + */ +export const playerSignalsFactory: t.PlayerSignalsFactory = (input = {}) => { + const defaults = wrangle.defaults(input); + const s = Signal.create; + + const props: T['props'] = { + ready: s(false), + + // Media: + src: s<t.StringVideoAddress>(defaults.src ?? D.video), + playing: s<boolean>(false), + currentTime: s<t.Secs>(0), + loop: s<boolean>(defaults.loop ?? D.loop), + autoPlay: s<boolean>(defaults.autoPlay ?? D.autoPlay), + muted: s<boolean>(defaults.muted ?? D.muted), + + // Appearance: + showControls: s<boolean>(defaults.showControls ?? D.showControls), + showFullscreenButton: s<boolean>(defaults.showFullscreenButton ?? D.showFullscreenButton), + showVolumeControl: s<boolean>(defaults.showVolumeControl ?? D.showVolumeControl), + background: s<boolean>(defaults.background ?? D.background), + cornerRadius: s<number>(defaults.cornerRadius ?? D.cornerRadius), + aspectRatio: s<string>(defaults.aspectRatio ?? D.aspectRatio), + scale: s<number | t.VideoPlayerScale>(defaults.scale ?? D.scale), + + // Commands: + jumpTo: s<t.VideoPlayerJumpTo | undefined>(), + }; + + const api: T = { + get props() { + return props; + }, + + /** + * Methods: + */ + jumpTo(second, options = {}) { + const { play = true } = options; + props.jumpTo.value = { second, play }; + return api; + }, + play: () => api.toggle(true), + pause: () => api.toggle(false), + toggle(playing) { + const next = typeof playing === 'boolean' ? playing : !props.playing.value; + props.playing.value = next; + return api; + }, + }; + + return api; +}; + +/** + * Helpers + */ +const wrangle = { + defaults( + input?: t.PlayerSignalsFactoryDefaults | t.StringVideoAddress, + ): t.PlayerSignalsFactoryDefaults { + if (!input) return {}; + if (typeof input === 'string') return { src: input }; + return input; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/mod.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/mod.ts new file mode 100644 index 0000000000..b86065fca4 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/mod.ts @@ -0,0 +1,6 @@ +/** + * @module + * A signal's controllable flexible `<VideoPlayer>` UI component. + */ +export { playerSignalsFactory } from './m.Signals.ts'; +export { VideoPlayer } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/t.Signals.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/t.Signals.ts new file mode 100644 index 0000000000..92ff936bbb --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/t.Signals.ts @@ -0,0 +1,81 @@ +import type { t } from './common.ts'; + +/** + * Signals: + */ +export type PlayerSignalsFactory = ( + defaults?: PlayerSignalsFactoryDefaults | t.StringVideoAddress, +) => t.VideoPlayerSignals; + +/** Defaults passed to the signals API factory. */ +export type PlayerSignalsFactoryDefaults = { + src?: t.StringVideoAddress; + loop?: boolean; + showControls?: boolean; + showFullscreenButton?: boolean; + showVolumeControl?: boolean; + cornerRadius?: number; + aspectRatio?: string; + scale?: number | t.VideoPlayerScale; + autoPlay?: boolean; + muted?: boolean; + background?: boolean; +}; + +/** + * Signals API for dynamic control of the <VideoPlayer>. + */ +export type VideoPlayerSignals = { + readonly props: VideoPlayerSignalProps; + jumpTo(second: t.Secs, options?: { play?: boolean }): VideoPlayerSignals; + play(): VideoPlayerSignals; + pause(): VideoPlayerSignals; + toggle(playing?: boolean): VideoPlayerSignals; +}; + +/** The raw signal properties that make up the VideoPlayer signals API/. */ +export type VideoPlayerSignalProps = { + readonly ready: t.Signal<boolean>; + readonly src: t.Signal<t.StringVideoAddress | undefined>; + + /** + * Media: + */ + readonly playing: t.Signal<boolean>; + readonly currentTime: t.Signal<t.Secs>; + readonly loop: t.Signal<boolean>; + readonly autoPlay: t.Signal<boolean>; + readonly muted: t.Signal<boolean>; + + /** + * Appearance: + */ + readonly showControls: t.Signal<boolean>; + readonly showFullscreenButton: t.Signal<boolean>; + readonly showVolumeControl: t.Signal<boolean>; + /** A background video, covers the container running silently (and generally auto-plays). */ + readonly background: t.Signal<boolean>; + readonly aspectRatio: t.Signal<string>; + readonly cornerRadius: t.Signal<number>; + readonly scale: t.Signal<number | t.VideoPlayerScale>; + + /** + * Commands: + */ + readonly jumpTo: t.Signal<t.VideoPlayerJumpTo | undefined>; +}; + +/** Structure representing a jump-to ("seek") location */ +export type VideoPlayerJumpTo = { second: t.Secs; play: boolean }; + +/** + * A function that calculates the scale transform to apply to the + * video-player based on + * + */ +export type VideoPlayerScale = (e: VideoPlayerScaleArgs) => t.Percent; +export type VideoPlayerScaleArgs = { + readonly width: t.Pixels; + readonly height: t.Pixels; + enlargeBy(increment: t.Pixels): t.Percent; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/t.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/t.ts new file mode 100644 index 0000000000..d4306edb8d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/t.ts @@ -0,0 +1,32 @@ +import type { MediaPlayerInstance, MediaPlayerProps } from '@vidstack/react'; +import type { RefObject } from 'react'; +import type { t } from './common.ts'; + +export type * from './t.Signals.ts'; + +/** The address of a video (eg. "vimeo/499921561"). */ +export type StringVideoAddress = string; + +/** A React reference to the MediaPlayer instance. */ +export type VideoPlayerRef = RefObject<MediaPlayerInstance>; + +/** Structure representing a jump-to ("seek") location */ +export type VideoPlayerJumpTo = { second: t.Secs; play: boolean }; + +/** + * Component: Video Player. + */ +export type VideoPlayerProps = { + debug?: boolean; + title?: string; + style?: t.CssInput; + + // Events: + onPlay?: MediaPlayerProps['onPlay']; + onPlaying?: MediaPlayerProps['onPlaying']; + onPause?: MediaPlayerProps['onPause']; + onEnded?: MediaPlayerProps['onEnded']; + + // State: + signals?: t.VideoPlayerSignals; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Player.Video/ui.tsx new file mode 100644 index 0000000000..e7d717c334 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/ui.tsx @@ -0,0 +1,168 @@ +import type { MediaPlayerInstance } from '@vidstack/react'; +import { MediaPlayer, MediaProvider } from '@vidstack/react'; +import { PlyrLayout, plyrLayoutIcons } from '@vidstack/react/player/layouts/plyr'; +import React, { useEffect, useRef, useState } from 'react'; + +import { type t, css, DEFAULTS, Signal, Style, useSizeObserver } from './common.ts'; +import { useSignalBinding } from './use.SignalBinding.ts'; +import { useThemeStyles } from './use.ThemeStyles.ts'; + +const D = DEFAULTS; + +/** + * Component: + */ +export const VideoPlayer: React.FC<t.VideoPlayerProps> = (props) => { + const { signals } = props; + const p = signals?.props; + + const src = p?.src.value ?? D.video; + const showControls = p?.showControls.value ?? D.showControls; + const showFullscreenButton = p?.showFullscreenButton.value ?? D.showFullscreenButton; + const showVolumeControl = p?.showVolumeControl?.value ?? D.showVolumeControl; + const autoPlay = p?.autoPlay.value ?? D.autoPlay; + const muted = autoPlay ? true : p?.muted.value ?? D.muted; + const aspectRatio = p?.aspectRatio.value ?? D.aspectRatio; + const cornerRadius = p?.cornerRadius.value ?? D.cornerRadius; + const scale = p?.scale.value ?? D.scale; + const loop = p?.loop.value ?? D.loop; + + const size = useSizeObserver(); + const [calcScale, setCalcScale] = useState<number>(); + + const [playerKey, setPlayerKey] = useState(0); + const playerRef = useRef<MediaPlayerInstance>(null); + useSignalBinding({ signals, playerRef }); + + /** + * Effect: ensure redraw on signal changes. + */ + Signal.useRedrawEffect(() => { + if (!p) return; + p.ready.value; + p.src.value; + + p.muted.value; + p.autoPlay.value; + p.loop.value; + + p.showControls.value; + p.showFullscreenButton.value; + p.showVolumeControl.value; + p.cornerRadius.value; + p.aspectRatio.value; + p.scale.value; + }); + + /** + * Effect: monitor size differene + */ + useEffect(() => { + const { width, height } = size; + const fn = p?.scale.value; + if (width === undefined || height === undefined) return; + if (typeof fn !== 'function') { + setCalcScale(undefined); + } else { + const enlargeBy = (increment: t.Pixels) => wrangle.scale(width, height, increment); + setCalcScale(fn({ width, height, enlargeBy })); + } + }, [size.width, size.height, scale]); + + /** + * Effect (HACK): ensure player style-sheets work consistently when bundled and deployed. + */ + useEffect(() => { + const sheet = Style.Dom.stylesheet(); + sheet.rule('[data-media-provider]', { width: '100%', height: '100%' }); + sheet.rule('[data-media-provider] iframe', { + width: '100%', + height: '100%', + objectFit: 'cover', + objectPosition: 'center', + }); + }, []); + + /** + * Render: + */ + const themeStyles = useThemeStyles('Plyr'); + const isReady = Boolean(themeStyles.loaded && !!p?.ready.value && size.ready); + const styles = { + base: css({ + overflow: 'hidden', + display: 'grid', + visibility: isReady ? 'visible' : 'hidden', // NB: avoid a FOUC ("Flash Of Unstyled Content"). + lineHeight: 0, // NB: ensure no "baseline" gap below the <MediaPlayer>. + }), + }; + + const elPlyrLayout = showControls && ( + <PlyrLayout + // thumbnails="https://files.vidstack.io/sprite-fight/thumbnails.vtt" + icons={plyrLayoutIcons} + slots={{ + settings: false, + fullscreenButton: showFullscreenButton ? undefined : false, + volumeSlider: showVolumeControl ? undefined : false, + }} + /> + ); + + const elPlayer = ( + <MediaPlayer + key={playerKey} + ref={playerRef} + style={{ + transform: `scale(${calcScale ?? scale ?? 1})`, + '--plyr-border-radius': `${cornerRadius}px`, + '--plyr-aspect-ratio': aspectRatio, // e.g. '4/3', '2.39/1', '1/1', etc... + }} + /** + * Props: + */ + title={props.title} + src={src} + playsInline={true} + aspectRatio={aspectRatio} + autoPlay={autoPlay} + muted={muted} + loop={loop} + /** + * Handlers: + */ + onPlay={props.onPlay} + onPlaying={props.onPlaying} + onPause={props.onPause} + onCanPlay={() => { + if (p) p.ready.value = true; + }} + onEnded={(e) => { + // Hack: force the Player back to the first-frame. + if (!loop) setPlayerKey((n) => n + 1); + props.onEnded?.(e); + }} + > + <MediaProvider /> + {elPlyrLayout} + </MediaPlayer> + ); + + return ( + <div ref={size.ref} className={css(styles.base, props.style).class}> + {elPlayer} + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + scale(width: t.Pixels, height: t.Pixels, increment: t.Pixels) { + if (width === 0 || height === 0) return 1; + const scaleX = (width + increment) / width; + const scaleY = (height + increment) / height; + return Math.max(scaleX, scaleY); // NB: Return the greater scale factor to ensure both dimensions are increased by at least increment. + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/use.SignalBinding.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/use.SignalBinding.ts new file mode 100644 index 0000000000..e03bb027b4 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/use.SignalBinding.ts @@ -0,0 +1,64 @@ +import { type MediaPlayerInstance, useMediaState } from '@vidstack/react'; +import React from 'react'; +import { type t, Signal } from './common.ts'; + +/** + * Manages keeping the <VideoPlayer> component in sync with + * the current state of the Signals API. + */ +export function useSignalBinding(args: { + playerRef: React.RefObject<MediaPlayerInstance>; + signals?: t.VideoPlayerSignals; +}) { + const { playerRef, signals } = args; + const props = signals?.props; + const player = playerRef.current || undefined; + + const currentTime = useMediaState('currentTime', playerRef); + const isPlaying = useMediaState('playing', playerRef); + + /** + * Effect: Keep the signal updated with the current "is-playing" state. + */ + React.useEffect(() => { + if (!props) return; + if (props.playing.value !== isPlaying) props.playing.value = isPlaying; + }, [isPlaying]); + + /** + * Effect: Sync the signals with the <Player>'s current state. + */ + React.useEffect(() => { + if (!props) return; + if (props.currentTime.value !== currentTime) props.currentTime.value = currentTime; + }, [currentTime, props]); + + /** + * Handle: jumpTo (aka. "seek"). + */ + Signal.useEffect(() => { + props?.ready.value; + const jumpTo = props?.jumpTo.value; + + if (player && props) { + if (typeof jumpTo?.second === 'number') { + player.currentTime = jumpTo.second; + if (jumpTo.play) player.play(); + if (!jumpTo.play) player.pause(); + props.jumpTo.value = undefined; // NB: reset after for next call. + } + } + }); + + /** + * Handle: play/pause. + */ + Signal.useEffect(() => { + props?.ready.value; + const isPlaying = props?.playing.value ?? false; + if (player) { + if (isPlaying && player.paused) player.play(); + if (!isPlaying && !player.paused) player.pause(); + } + }); +} diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/use.ThemeStyles.plyr.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/use.ThemeStyles.plyr.ts new file mode 100644 index 0000000000..fc677cd720 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/use.ThemeStyles.plyr.ts @@ -0,0 +1,2 @@ +import '@vidstack/react/player/styles/base.css'; +import '@vidstack/react/player/styles/plyr/theme.css'; diff --git a/code/sys.ui/ui-react-components/src/ui/Player.Video/use.ThemeStyles.ts b/code/sys.ui/ui-react-components/src/ui/Player.Video/use.ThemeStyles.ts new file mode 100644 index 0000000000..eda62f5cff --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player.Video/use.ThemeStyles.ts @@ -0,0 +1,45 @@ +import React from 'react'; +import { Is } from './common.ts'; + +export type ThemeKind = 'Plyr'; + +/** + * NOTE: Conditionally load styles only when running within browser. + * + * Why? Prevents throwing an import error on the server (Deno) + * which occurs when running unit-tests. + * + * 𫵠CONSIDER: + * Handling the .css imports within the Vite bundle (?) + * which would prevent a FOUC ("Flash Of Unstyled Content") + * that does not happen, rather the component just renders + * nothing until the `Styles.loaded` is [true]. + */ +export function useThemeStyles(kind: ThemeKind) { + const [loaded, setLoaded] = React.useState(false); + + React.useEffect(() => { + if (!Is.browser()) { + setLoaded(true); + return; + } + + let max = 0; + let count = 0; + const onLoaded = () => { + count++; + if (count >= max) setLoaded(true); + }; + + if (kind === 'Plyr') { + max = 1; + import('./use.ThemeStyles.plyr.ts').then(onLoaded); + return; + } + + throw new Error(`Theme "${kind}" not supported.`); + }, []); + + // API. + return { loaded } as const; +} diff --git a/code/sys.ui/ui-react-components/src/ui/Player/-.test.ts b/code/sys.ui/ui-react-components/src/ui/Player/-.test.ts new file mode 100644 index 0000000000..3abf8c20f5 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player/-.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from '../../-test.ts'; +import { ConceptPlayer, VideoPlayer } from '../../mod.ts'; +import { Thumbnails } from '../Player.Thumbnails/mod.ts'; +import { Player } from './mod.ts'; + +describe('Player', () => { + it('API', () => { + expect(Player.Concept.View).to.equal(ConceptPlayer); + expect(Player.Video.View).to.equal(VideoPlayer); + expect(Player.Timestamp.Thumbnails.View).to.equal(Thumbnails); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Player/common.ts b/code/sys.ui/ui-react-components/src/ui/Player/common.ts new file mode 100644 index 0000000000..57e74207ff --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player/common.ts @@ -0,0 +1,3 @@ +export const PlayerColors = { + BLUE: '#4BA3E7', +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Player/m.Player.ts b/code/sys.ui/ui-react-components/src/ui/Player/m.Player.ts new file mode 100644 index 0000000000..4b4cf7b176 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player/m.Player.ts @@ -0,0 +1,18 @@ +import { type t } from '../common.ts'; + +import { ConceptPlayer } from '../Player.Concept/mod.ts'; +import { Thumbnails } from '../Player.Thumbnails/mod.ts'; +import { VideoPlayer, playerSignalsFactory } from '../Player.Video/mod.ts'; + +export const Player: t.PlayerLib = { + Concept: { + View: ConceptPlayer, + }, + Video: { + View: VideoPlayer, + signals: playerSignalsFactory, + }, + Timestamp: { + Thumbnails: { View: Thumbnails }, + }, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Player/mod.ts b/code/sys.ui/ui-react-components/src/ui/Player/mod.ts new file mode 100644 index 0000000000..a65930b1fc --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player/mod.ts @@ -0,0 +1,8 @@ +/** + * @module + * "Player" namespace + * - Video + * - Concept + * - Timestamps / Thumbnails + */ +export { Player } from './m.Player.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Player/t.ts b/code/sys.ui/ui-react-components/src/ui/Player/t.ts new file mode 100644 index 0000000000..1b2dcc6484 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Player/t.ts @@ -0,0 +1,18 @@ +import type React from 'react'; +import type { t } from '../common.ts'; + +/** + * Library: Players + */ +export type PlayerLib = { + Concept: { + View: React.FC<t.ConceptPlayerProps>; + }; + Video: { + View: React.FC<t.VideoPlayerProps>; + signals: t.PlayerSignalsFactory; + }; + Timestamp: { + Thumbnails: { View: React.FC<t.ThumbnailsProps> }; + }; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Preload/-SPEC.Debug.tsx new file mode 100644 index 0000000000..3d3b10bf22 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/-SPEC.Debug.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; + +import { type t, css, Signal } from './common.ts'; +import { Preload } from './mod.ts'; + +type O = t.PreloadOptions; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + disposeDelay: s<O['disposeDelay']>(500), + }; + const api = { + props, + listen() { + const p = props; + p.disposeDelay.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'PreloadPortal'}</div> + </div> + + <Button + block + label={() => { + const msecs = p.disposeDelay.value; + return `disposeAfter: ${msecs !== undefined ? `${msecs}ms` : '<undefined>'}`; + }} + onClick={() => Signal.cycle<O['disposeDelay']>(p.disposeDelay, [undefined, 0, 500, 2000])} + /> + + <hr /> + + <Button + block + label={() => 'Preload.render'} + onClick={async () => { + const disposeAfter = p.disposeDelay.value; + const el = <div>Hello</div>; + const res = await Preload.render(el, { disposeDelay: disposeAfter }); + console.info(res); + }} + /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Preload/-SPEC.tsx new file mode 100644 index 0000000000..2cbb775c3a --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/-SPEC.tsx @@ -0,0 +1,23 @@ +import { Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; + +export default Spec.describe('PreloadPortal', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject.size().display('grid'); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/common.ts b/code/sys.ui/ui-react-components/src/ui/Preload/common.ts new file mode 100644 index 0000000000..a45006948c --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/common.ts @@ -0,0 +1,7 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; +export const D = DEFAULTS; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/m.Preload.ts b/code/sys.ui/ui-react-components/src/ui/Preload/m.Preload.ts new file mode 100644 index 0000000000..55a602e148 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/m.Preload.ts @@ -0,0 +1,8 @@ +import type { t } from './common.ts'; +import { render } from './u.render.tsx'; +import { PreloadPortal as Portal } from './ui.tsx'; + +export const Preload: t.PreloadLib = { + Portal, + render, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/mod.ts b/code/sys.ui/ui-react-components/src/ui/Preload/mod.ts new file mode 100644 index 0000000000..ec4100c734 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { Preload } from './m.Preload.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/t.ts b/code/sys.ui/ui-react-components/src/ui/Preload/t.ts new file mode 100644 index 0000000000..d3124a5d2c --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/t.ts @@ -0,0 +1,30 @@ +import type { t } from './common.ts'; + +/** + * Preload rendering helpers. + */ +export type PreloadLib = { + render: Preload; + Portal: React.FC<PreloadPortalProps>; +}; + +/** + * <Component>: + * A portal container used for pre-loading component content off screen. + */ +export type PreloadPortalProps = { + children: React.ReactNode; + size?: t.SizeTuple; +}; + +/** + * Preload function that performs all DOM insertion and clean up. + */ +export type Preload = ( + children: React.ReactNode, + options?: t.PreloadOptions | t.Msecs, +) => Promise<t.Lifecycle>; +export type PreloadOptions = { + disposeDelay?: t.Msecs; + size?: t.SizeTuple; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/u.render.tsx b/code/sys.ui/ui-react-components/src/ui/Preload/u.render.tsx new file mode 100644 index 0000000000..52a7d0e3ba --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/u.render.tsx @@ -0,0 +1,42 @@ +import { createRoot } from 'react-dom/client'; + +import { type t, Time, rx } from './common.ts'; +import { PreloadPortal } from './ui.tsx'; + +export const render: t.Preload = async (children, op) => { + const life = rx.lifecycle(); + const { size, disposeDelay } = wrangle.options(op); + + if (typeof document === 'undefined') { + life.dispose(); + return life; + } + + life.dispose$.subscribe(() => { + root.unmount(); + document.body.removeChild(div); + }); + + /** + * Render Portal: + */ + const div = document.createElement('div'); + document.body.appendChild(div); + const root = createRoot(div); + root.render(<PreloadPortal size={size}>{children}</PreloadPortal>); + + // Finish up. + if (typeof disposeDelay === 'number') await Time.delay(disposeDelay, life.dispose); + return life; +}; + +/** + * Helpers + */ +const wrangle = { + options(input?: t.PreloadOptions | t.Msecs): t.PreloadOptions { + if (!input) return {}; + if (typeof input === 'number') return { disposeDelay: input }; + return input; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Preload/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Preload/ui.tsx new file mode 100644 index 0000000000..a0d00e8709 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Preload/ui.tsx @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { type t } from './common.ts'; + +const OFFSCREEN = '-99999px'; + +export const PreloadPortal: React.FC<t.PreloadPortalProps> = (props) => { + const { size = [0, 0] } = props; + const [container, setContainer] = useState<HTMLDivElement | null>(null); + + /** + * Effect: + */ + useEffect(() => { + const el = document.createElement('div'); + el.setAttribute('data-component', 'sys.preload'); + + el.style.position = 'absolute'; + el.style.left = OFFSCREEN; + el.style.top = OFFSCREEN; + el.style.width = `${size[0]}px`; + el.style.height = `${size[1]}px`; + el.style.overflow = 'hidden'; + el.style.opacity = '0'; + + document.body.appendChild(el); + setContainer(el); + + const dispose = () => { + if (el.parentElement) { + document.body.removeChild(el); + setContainer(null); + } + }; + + // Finish up. + return dispose; + }, [size.join()]); + + /** + * Render: + */ + if (!container) return null; + return createPortal(props.children, container); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/-.test.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/-.test.ts new file mode 100644 index 0000000000..a48fa45e04 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/-.test.ts @@ -0,0 +1,186 @@ +import { describe, expect, it, Signal } from '../../-test.ts'; +// import { VIDEO } from '../App.Content/mod.ts'; +import { Sheet } from './mod.ts'; + +type T = { id: string }; + +describe('Sheet', () => { + describe('Sheet.Signals:stack', () => { + const a: T = { id: 'a' }; + const b: T = { id: 'b' }; + const c: T = { id: 'c' }; + + describe('factory', () => { + it('passed in signal (param)', () => { + const signal = Signal.create<T[]>([]); + const state = Sheet.Signals.stack(signal); + + expect(state.length).to.eql(0); + expect(state.items).to.eql([]); + expect(state.items).to.not.equal(signal.value); // NB: cloned array - protect from mutation. + expect(state.toSignal()).to.equal(signal); + + signal.value = [{ id: 'foo' }]; + expect(state.items).to.eql([{ id: 'foo' }]); + }); + + it('generate signal (no param)', () => { + const state = Sheet.Signals.stack(); + + expect(state.length).to.eql(0); + state.toSignal().value = [{ id: 'foo' }]; + expect(state.items).to.eql([{ id: 'foo' }]); + }); + }); + + describe('method: push', () => { + it('push (1) - triggers signal effect', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + const fired: number[] = []; + Signal.effect(() => { + fired.push(signal.value.length); + }); + + expect(stack.length).to.eql(0); + stack.push(a); + expect(stack.length).to.eql(1); + expect(signal.value).to.eql([a]); + expect(fired).to.eql([0, 1]); + + stack.push(b); + expect(stack.length).to.eql(2); + expect(signal.value).to.eql([a, b]); + expect(fired).to.eql([0, 1, 2]); + }); + + it('push (many)', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + const fired: number[] = []; + Signal.effect(() => { + fired.push(signal.value.length); + }); + + stack.push(a); + expect(stack.length).to.eql(1); + expect(fired).to.eql([0, 1]); + + stack.push(b, c); + expect(stack.length).to.eql(3); + expect(signal.value).to.eql([a, b, c]); + expect(fired).to.eql([0, 1, 3]); + }); + + it('push <undefined>', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + stack.push(); + stack.push(undefined, a, undefined, b); + expect(stack.length).to.eql(2); + expect(signal.value).to.eql([a, b]); + }); + }); + + describe('method: clear', () => { + it('clear all', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + const fired: number[] = []; + Signal.effect(() => { + fired.push(signal.value.length); + }); + + stack.push(a, b); + expect(signal.value).to.eql([a, b]); + expect(fired).to.eql([0, 2]); + expect(stack.length).to.eql(2); + + stack.clear(); + expect(fired).to.eql([0, 2, 0]); + expect(stack.length).to.eql(0); + }); + + it('clear( leave )', () => { + const test = (leave: number) => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + stack.push(a, b, c); + expect(stack.length).to.eql(3); + + stack.clear(leave); + expect(stack.length).to.eql(leave); + }; + test(0); + test(1); + test(2); + }); + }); + + it('method: exists', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + stack.push(a, b); + + expect(stack.exists((m) => m.id === a.id)).to.eql(true); + expect(stack.exists((m) => m.id === b.id)).to.eql(true); + expect(stack.exists((m) => m.id === c.id)).to.eql(false); + }); + + describe('method: pop', () => { + it('pop: removes top most layer', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + const fired: number[] = []; + Signal.effect(() => { + fired.push(signal.value.length); + }); + + stack.push(a, b, c); + expect(fired).to.eql([0, 3]); + + stack.pop(); + expect(fired).to.eql([0, 3, 2]); + expect(signal.value).to.eql([a, b]); + + stack.pop(); + stack.pop(); + expect(fired).to.eql([0, 3, 2, 1, 0]); + expect(signal.value).to.eql([]); + + stack.pop(); // NB: ā already empty at this point. + expect(signal.value).to.eql([]); + + expect(fired.length).to.eql(5); + stack.pop(); + stack.pop(); + expect(fired.length).to.eql(5); // NB: no change (already empty). + }); + + it('pop( leave ) ā retain minimum level', () => { + const signal = Signal.create<T[]>([]); + const stack = Sheet.Signals.stack(signal); + stack.push(a, b, c); + expect(stack.length).to.eql(3); + + stack.pop(3); + stack.pop(3); + expect(stack.length).to.eql(3); + + stack.pop(); + expect(stack.length).to.eql(2); + + stack.pop(1); + stack.pop(1); + stack.pop(1); + expect(stack.length).to.eql(1); + + stack.pop(-1); + expect(stack.length).to.eql(0); + stack.pop(-1); + stack.pop(-1); + expect(stack.length).to.eql(0); + }); + }); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Sheet/-SPEC.Debug.tsx new file mode 100644 index 0000000000..5c6e6c810f --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/-SPEC.Debug.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, css, DEFAULTS, Signal } from './common.ts'; + +type P = t.SheetProps; +const D = DEFAULTS; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + + const props = { + showing: s(true), + theme: s<P['theme']>('Dark'), + orientation: s<P['orientation']>(D.orientation.default), + edgeMargin: s<P['edgeMargin']>(0), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.showing.value; + p.orientation.value; + p.edgeMargin.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const d = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.title.class}>{'Mobile Sheet'}</div> + <Button + block + label={`debug.showing: ${d.showing}`} + onClick={() => Signal.toggle(d.showing)} + /> + <hr /> + <Button + block + label={`theme: ${d.theme}`} + onClick={() => Signal.cycle<P['theme']>(d.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`orientation: ${d.orientation.value ?? '<undefined>'}`} + onClick={() => { + Signal.cycle<P['orientation']>(d.orientation, [...D.orientation.all, undefined]); + }} + /> + <Button + block + label={`edgeMargin: ${d.edgeMargin.value ?? '<undefined>'}`} + onClick={() => { + type T = t.SheetMarginInput; + const values: (T | undefined)[] = [undefined, 0, 10, [50, 15], ['1fr', 200, '2em']]; + Signal.cycle<P['edgeMargin']>(d.edgeMargin, values); + }} + /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Sheet/-SPEC.tsx new file mode 100644 index 0000000000..5a5d641fe8 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/-SPEC.tsx @@ -0,0 +1,60 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { AnimatePresence, Color, css } from './common.ts'; +import { Sheet } from './mod.ts'; + +export default Spec.describe('Sheet', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size([390, 844]) + .display('grid') + + .render((e) => { + const orientation = p.orientation.value; + const isShowing = p.showing.value; + const styles = { + base: css({ overflow: 'hidden', display: 'grid' }), + sheet: css({ padding: 15, userSelect: 'none' }), + dim: css({ opacity: 0.3 }), + }; + + const elSheet = isShowing && ( + <Sheet.View + theme={Color.Theme.invert(p.theme.value)} + orientation={orientation} + edgeMargin={p.edgeMargin.value} + onMouseDown={(e) => { + e.stopPropagation(); + p.showing.value = false; + }} + > + <div className={styles.sheet.class}> + {'š MySheet'} ā <span className={styles.dim.class}>{'(click to hide)'}</span> + </div> + </Sheet.View> + ); + + return ( + <div className={styles.base.class} onMouseDown={() => (p.showing.value = true)}> + <AnimatePresence>{elSheet}</AnimatePresence> + </div> + ); + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/common.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/common.ts new file mode 100644 index 0000000000..4df62bf8a2 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/common.ts @@ -0,0 +1,20 @@ +import { type t } from './common.ts'; +export * from '../common.ts'; + +/** + * Constants: + */ + +export const DEFAULTS = { + radius: 4, + duration: 0.25, + bounce: 0.2, + shadowColor: -0.15, + + get orientation(): { all: t.SheetOrientation[]; default: t.SheetOrientation } { + return { + default: 'Bottom:Up', + all: ['Bottom:Up', 'Top:Down', 'Left:Right', 'Right:Left'], + }; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/m.Signals.Stack.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/m.Signals.Stack.ts new file mode 100644 index 0000000000..3f5a3fbf70 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/m.Signals.Stack.ts @@ -0,0 +1,39 @@ +import { type t, Signal } from './common.ts'; + +/** + * Wrap the given signal with the [Stack] API. + */ +export const createStack: t.SheetSignalsLib['stack'] = <T>(input?: t.Signal<T[]>) => { + const signal = input ?? Signal.create<T[]>([]); + + const api: t.SheetSignalStack<T> = { + get length() { + return signal.value.length; + }, + get items() { + return [...signal.value]; + }, + + push(...content) { + const next = [...signal.value, ...content].filter(Boolean) as T[]; + signal.value = next; + }, + pop(leave = 0) { + const stack = signal; + if (stack.value.length > leave) stack.value = stack.value.slice(0, -1); + }, + clear(leave = 0) { + signal.value = signal.value.slice(0, leave); + }, + + exists(fn) { + return api.items.some(fn); + }, + + toSignal() { + return signal; + }, + }; + + return api; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/m.Signals.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/m.Signals.ts new file mode 100644 index 0000000000..e140971d1b --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/m.Signals.ts @@ -0,0 +1,9 @@ +import { type t } from './common.ts'; +import { createStack } from './m.Signals.Stack.ts'; + +/** + * Library: Sheet Signals (State). + */ +export const Signals: t.SheetSignalsLib = { + stack: createStack, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/mod.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/mod.ts new file mode 100644 index 0000000000..c84d6ac66e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/mod.ts @@ -0,0 +1,12 @@ +/** + * @module + * An animated sliding "sheet" content container. + */ +import type { t } from './common.ts'; +import { Sheet as View } from './ui.tsx'; +import { Signals } from './m.Signals.ts'; + +export const Sheet: t.SheetLib = { + View, + Signals, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/t.Signals.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/t.Signals.ts new file mode 100644 index 0000000000..22719779f0 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/t.Signals.ts @@ -0,0 +1,35 @@ +import type { t } from './common.ts'; + +/** + * Library: Sheet Signals (State). + */ +export type SheetSignalsLib = { + /** Factory: create a new stack API. */ + stack<T>(signal?: t.Signal<T[]>): t.SheetSignalStack<T>; +}; + +/** + * API for managing the stack of sheets. + */ +export type SheetSignalStack<T> = { + /** The number of items in the stack. */ + readonly length: number; + + /** The list of screens. */ + readonly items: T[]; + + /** Determine if the item exists within the stack. */ + exists(fn: (e: T) => boolean): boolean; + + /** Add a screen to the top of the stack. */ + push(...content: (T | undefined)[]): void; + + /** Remove the highest screen from the stack. */ + pop(leave?: number): void; + + /** Removes all screens from the stack. */ + clear(leave?: number): void; + + /** Extract the underlying signal. */ + toSignal(): t.Signal<T[]>; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/t.ts b/code/sys.ui/ui-react-components/src/ui/Sheet/t.ts new file mode 100644 index 0000000000..ef57ad5d3c --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/t.ts @@ -0,0 +1,58 @@ +import type React from 'react'; +import type { t } from './common.ts'; + +export type * from './t.Signals.ts'; +type M = React.MouseEventHandler<HTMLDivElement>; + +/** + * Library: Sheet + */ +export type SheetLib = { + /** The UI component of the sheet. */ + readonly View: React.FC<SheetProps>; + + /** Library: Sheet Signals (State) */ + readonly Signals: t.SheetSignalsLib; +}; + +/** + * The orientation and slide direction of the sheet. + */ +export type SheetOrientation = SheetOrientationX | SheetOrientationY; +export type SheetOrientationX = 'Left:Right' | 'Right:Left'; +export type SheetOrientationY = 'Bottom:Up' | 'Top:Down'; + +/** + * <Component>: + */ +export type SheetProps = { + children?: t.ReactNode; + radius?: t.Pixels; + + orientation?: t.SheetOrientation; + duration?: t.Secs; + bounce?: number; + + edgeMargin?: t.SheetMarginInput; + shadowOpacity?: t.Percent; + pointerEvents?: t.CssProps['pointerEvents']; + theme?: t.CommonTheme; + style?: t.CssInput; + + onClick?: M; + onDoubleClick?: M; + onMouseDown?: M; + onMouseUp?: M; + onMouseEnter?: M; + onMouseLeave?: M; +}; + +/** + * Margins for the sheet edges based on orientation. + * A pixel value or a CSS-grid template value (eg. 'auto' or '1fr' etc). + */ +export type SheetMargin = t.Pixels | string; +export type SheetMarginInput = + | [SheetMargin, SheetMargin, SheetMargin] // ā near | middle | far + | [SheetMargin, SheetMargin] // ā near | (default: 1fr) | far + | SheetMargin; // ā near | (default: 1fr) | far diff --git a/code/sys.ui/ui-react-components/src/ui/Sheet/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Sheet/ui.tsx new file mode 100644 index 0000000000..89448b6844 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Sheet/ui.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { type t, Color, css, DEFAULTS as D, M } from './common.ts'; + +type P = t.SheetProps; + +export const Sheet: React.FC<P> = (props) => { + const { duration = D.duration, bounce = D.bounce, pointerEvents = 'auto' } = props; + const is = wrangle.is(props); + const animation = wrangle.animation(props); + const { borderRadius, boxShadow, gridTemplateColumns, gridTemplateRows } = wrangle.styles(props); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const backgroundColor = theme.bg; + const styles = { + base: css({ position: 'relative', display: 'grid', gridTemplateColumns, gridTemplateRows }), + spacer: css({ pointerEvents: 'none' }), + body: css({ + position: 'relative', + pointerEvents, + color: theme.fg, + backgroundColor, + borderRadius, + boxShadow, + display: 'grid', + }), + mask: css({ + // NB: Extends the sheet to ensure the physics bounce does not show a flash. + // Ensure this component is within a container with { overflow: 'hidden' }. + ...wrangle.maskOrientation(props), + backgroundColor, + }), + }; + + const elBody = ( + <div className={styles.body.class}> + <div className={styles.mask.class} /> + {props.children} + </div> + ); + + return ( + <M.div + data-component={`sys.ui.sheet`} + className={css(styles.base, props.style).class} + /** + * Animation: + */ + initial={animation.initial} + animate={is.vertical ? { y: '0%' } : { x: '0%' }} + exit={animation.exit} + transition={{ duration, type: 'spring', bounce }} + /** + * Handlers: + */ + onClick={props.onClick} + onDoubleClick={props.onDoubleClick} + onMouseDown={props.onMouseDown} + onMouseUp={props.onMouseUp} + onMouseEnter={props.onMouseEnter} + onMouseLeave={props.onMouseLeave} + > + <div className={styles.spacer.class} /> + {elBody} + <div className={styles.spacer.class} /> + </M.div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + is(props: P) { + const { orientation = D.orientation.default } = props; + return { + vertical: orientation === 'Top:Down' || orientation === 'Bottom:Up', + topDown: orientation === 'Top:Down', + leftToRight: orientation === 'Left:Right', + } as const; + }, + + styles(props: P) { + const { radius = D.radius } = props; + const is = wrangle.is(props); + const shadowColor = Color.format(props.shadowOpacity ?? D.shadowColor); + const edgeMargin = wrangle.edgeMarginTemplate(props); + + let borderRadius: string; + let boxShadow: string; + let gridTemplateColumns: string | undefined; + let gridTemplateRows: string | undefined; + + if (is.vertical) { + gridTemplateColumns = edgeMargin; + if (is.topDown) { + borderRadius = `0 0 ${radius}px ${radius}px`; + boxShadow = `0 5px 6px 0 ${shadowColor}`; + } else { + borderRadius = `${radius}px ${radius}px 0 0`; + boxShadow = `0 -5px 6px 0 ${shadowColor}`; + } + } else { + gridTemplateRows = edgeMargin; + if (is.leftToRight) { + borderRadius = `0 ${radius}px ${radius}px 0`; + boxShadow = `5px 0 6px 0 ${shadowColor}`; + } else { + borderRadius = `${radius}px 0 0 ${radius}px`; + boxShadow = `-5px 0 6px 0 ${shadowColor}`; + } + } + + return { borderRadius, boxShadow, gridTemplateColumns, gridTemplateRows } as const; + }, + + edgeMargin(props: P): [t.SheetMargin, t.SheetMargin, t.SheetMargin] { + const v = props.edgeMargin; + if (typeof v === 'number') return [v, '1fr', v]; + if (Array.isArray(v)) { + if (v.length === 2) return [v[0], '1fr', v[1]]; + if (v.length === 3) return v; + } + return [0, '1fr', 0]; + }, + + edgeMarginTemplate(props: P): string { + const margin = wrangle.edgeMargin(props); + return margin.map((v) => (typeof v === 'number' ? `${v}px` : v)).join(' '); + }, + + maskOrientation(props: P): t.CssValue { + const is = wrangle.is(props); + const buffer = 30; + if (is.vertical) { + return { + Absolute: is.topDown ? [-buffer, 0, null, 0] : [null, 0, -buffer, 0], + height: buffer, + }; + } else { + return { + Absolute: is.leftToRight ? [0, null, 0, -buffer] : [0, -buffer, 0, null], + width: buffer, + }; + } + }, + + animation(props: P) { + const is = wrangle.is(props); + const buffer = '10px'; + let initial: Record<string, string>, exit: Record<string, string>; + if (is.vertical) { + if (is.topDown) { + // 'Top:Down' + initial = { y: `calc(-100% - ${buffer})` }; + exit = { y: `calc(-100% - ${buffer})` }; + } else { + // 'Bottom:Up' + initial = { y: `calc(100% + ${buffer})` }; + exit = { y: `calc(100% + ${buffer})` }; + } + } else { + if (is.leftToRight) { + // 'Left:Right' + initial = { x: `calc(-100% - ${buffer})` }; + exit = { x: `calc(-100% - ${buffer})` }; + } else { + // 'Right:Left' + initial = { x: `calc(100% + ${buffer})` }; + exit = { x: `calc(100% + ${buffer})` }; + } + } + return { initial, exit } as const; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/-SPEC.Debug.tsx new file mode 100644 index 0000000000..4901beca46 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/-SPEC.Debug.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Button } from '../Button/mod.ts'; +import { type t, css, D, Signal } from './common.ts'; + +type P = t.BarSpinnerProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + width: s<P['width']>(), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.width.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'Spinners.Bar'}</div> + <div /> + <div></div> + </div> + + <Button + block + label={() => `theme: ${p.theme.value ?? '<undefined>'}`} + onClick={() => Signal.cycle<P['theme']>(p.theme, ['Light', 'Dark'])} + /> + + <Button + block + label={() => `width: ${p.width.value ?? '<undefined>'}`} + onClick={() => Signal.cycle<P['width']>(p.width, [undefined, 30, D.width, 300])} + /> + + <hr /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/-SPEC.tsx new file mode 100644 index 0000000000..8ff0e73b55 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/-SPEC.tsx @@ -0,0 +1,34 @@ +import { css, Dev, Signal, Spec } from '../-test.ui.ts'; +import { Spinners } from '../Spinners/mod.ts'; +import { createDebugSignals, Debug } from './-SPEC.Debug.tsx'; + +export default Spec.describe('Spinners', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => { + return ( + <div className={css({ display: 'grid', placeItems: 'center' }).class}> + <Spinners.Bar theme={p.theme.value} width={p.width.value} /> + </div> + ); + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/common.ts b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/common.ts new file mode 100644 index 0000000000..192e496bbd --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/common.ts @@ -0,0 +1,7 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { width: 80 } as const; +export const D = DEFAULTS; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/mod.ts b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/mod.ts new file mode 100644 index 0000000000..f744b5e57d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { BarSpinner } from './ui.tsx'; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/t.ts b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/t.ts new file mode 100644 index 0000000000..67ef8e9f89 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/t.ts @@ -0,0 +1,10 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type BarSpinnerProps = { + width?: number; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/ui.tsx b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/ui.tsx new file mode 100644 index 0000000000..70c1a65b13 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners.Bar/ui.tsx @@ -0,0 +1,44 @@ +import React, { useEffect } from 'react'; +import { type t, Color, css, D } from './common.ts'; + +type BarLoaderProps = { color?: string; width?: number }; +let BarLoader: React.ComponentType<BarLoaderProps> | undefined; + +export const BarSpinner: React.FC<t.BarSpinnerProps> = (props) => { + const { width = D.width } = props; + + /** + * Effect: + */ + useEffect(() => { + /** + * HACK: Errors when this file is parsed within Deno on the server + * (because CJS not ESM (??)) + * Only import the component when within a browser. + */ + if (!globalThis.window) return; + import('react-spinners').then((e) => (BarLoader = e.BarLoader)); + }, []); + + if (!BarLoader) return null; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + position: 'relative', + overflow: 'hidden', + borderRadius: 10, + opacity: 0.5, + width, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <BarLoader color={theme.fg} width={width} /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners/common.ts b/code/sys.ui/ui-react-components/src/ui/Spinners/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners/mod.ts b/code/sys.ui/ui-react-components/src/ui/Spinners/mod.ts new file mode 100644 index 0000000000..2974b16429 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners/mod.ts @@ -0,0 +1,10 @@ +/** + * @module + * UI load spinners. + */ +import type { t } from './common.ts'; +import { BarSpinner as Bar } from '../Spinners.Bar/mod.ts'; + +export const Spinners: t.SpinnerLib = { + Bar, +}; diff --git a/code/sys.ui/ui-react-components/src/ui/Spinners/t.ts b/code/sys.ui/ui-react-components/src/ui/Spinners/t.ts new file mode 100644 index 0000000000..ed22a7e408 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/Spinners/t.ts @@ -0,0 +1,8 @@ +import type { t } from './common.ts'; + +/** + * UI load spinners. + */ +export type SpinnerLib = { + Bar: React.FC<t.BarSpinnerProps>; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/-SPEC.Debug.tsx b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/-SPEC.Debug.tsx new file mode 100644 index 0000000000..5707243917 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/-SPEC.Debug.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { VIMEO } from '../-test.ui.ts'; +import { Button } from '../Button/mod.ts'; +import { type t, Color, css, Signal, DEFAULTS, D } from './common.ts'; + +type P = t.VimeoBackgroundProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + video: s<P['video']>(VIMEO['app/tubes']), + blur: s<P['blur']>(), + opacity: s<P['opacity']>(), + playing: s<P['playing']>(DEFAULTS.playing), + playingTransition: s<P['playingTransition']>(), + jumpTo: s<P['jumpTo']>(), + vimeo: s<t.VimeoIFrame>(), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.video.value; + p.opacity.value; + p.blur.value; + p.playing.value; + p.playingTransition.value; + p.jumpTo.value; + p.vimeo.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => { + debug.listen(); + }); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + title: css({ fontWeight: 'bold' }), + }; + + const srcVideo = (key?: keyof typeof VIMEO) => { + const id = key ? VIMEO[key] : undefined; + return ( + <Button + block + label={key ?? '<undefined>'} + onClick={() => { + p.video.value = id; // NB: both valid input. + p.video.value = `vimeo/${id}`; + }} + /> + ); + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`theme: "${p.theme}"`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`opacity: ${p.opacity.value ?? '<undefined>'}`} + onClick={() => { + Signal.cycle<P['opacity']>(p.opacity, [undefined, 0, 0.5, 1]); + }} + /> + <Button + block + label={`blur: ${p.blur.value ?? '<undefined>'}`} + onClick={() => { + Signal.cycle<P['blur']>(p.blur, [undefined, 5, 15, 60]); + }} + /> + + <hr /> + <Button + block + label={`playing: ${p.playing.value}`} + onClick={() => Signal.toggle(p.playing)} + /> + <Button + block + label={() => { + const v = p.playingTransition.value; + return `playingTransition (ms): ${v ?? `<undefined> (${D.playingTransition})`}`; + }} + onClick={() => { + Signal.cycle<P['playingTransition']>(p.playingTransition, [1000, 2000, undefined]); + }} + /> + <Button + block + label={`jumpTo: ${p.jumpTo.value ?? '<undefined>'}`} + onClick={() => { + Signal.cycle<P['jumpTo']>(p.jumpTo, [undefined, 0, 10]); + }} + /> + + <hr /> + <div className={styles.title.class}>{`video: ${p.video.value}`}</div> + {srcVideo('app/tubes')} + {srcVideo('stock/running')} + {srcVideo('public/helvetica')} + + <hr /> + + <div className={styles.title.class}>API</div> + + <Button + block + label={`get: current time (seconds)`} + onClick={async () => { + const api = p.vimeo.value; + console.info('get.time:', await api?.get.time()); + }} + /> + </div> + ); +}; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/-SPEC.tsx b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/-SPEC.tsx new file mode 100644 index 0000000000..700f01fc1a --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/-SPEC.tsx @@ -0,0 +1,42 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { VimeoBackground } from './mod.ts'; + +export default Spec.describe('VimeoBackground', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => ( + <VimeoBackground + theme={p.theme.value} + video={p.video.value} + opacity={p.opacity.value} + blur={p.blur.value} + playing={p.playing.value} + playingTransition={p.playingTransition.value} + jumpTo={p.jumpTo.value} + onReady={(api) => { + console.info(`ā”ļø onReady`, api); + debug.props.vimeo.value = api; + }} + /> + )); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/api.Vimeo.ts b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/api.Vimeo.ts new file mode 100644 index 0000000000..1f7e1ede2e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/api.Vimeo.ts @@ -0,0 +1,59 @@ +import { type t, Time } from './common.ts'; + +/** + * Vimeo simple IFrame/message API. + */ +export const Vimeo = { + create(iframe: HTMLIFrameElement, options: { timeout?: t.Msecs } = {}): t.VimeoIFrame { + const { timeout = 3_000 } = options; + + const api: t.VimeoIFrame = { + post(method: string, value?: number | string) { + const targetOrigin = '*'; // NB: Using "*" as targetOrigin is acceptable when not validating the origin. + iframe?.contentWindow?.postMessage({ method, value }, targetOrigin); + }, + get: { + method<T>(method: string, dispose$: t.UntilInput) { + return new Promise<T>((resolve, reject) => { + const failWith = (message: string) => reject(new Error(message)); + const listener = (event: MessageEvent) => { + if (event.origin !== 'https://player.vimeo.com') return; // Ignore: not the Vimeo player iframe. + + const data = event.data || {}; + if (data.method === method) { + cleanup(); + resolve(data.value as T); + } + }; + + window.addEventListener('message', listener); + const cleanup = () => window.removeEventListener('message', listener); + + api.post(method); + Time.until(dispose$).delay(timeout, () => { + cleanup(); + failWith(`Timeout waiting for method: ${method}`); + }); + }); + }, + + currentTime(dispose$) { + return api.get.method<number>('getCurrentTime', dispose$); + }, + + duration(dispose$) { + return api.get.method<number>('getDuration', dispose$); + }, + + async time(dispose$) { + const [current, duration] = await Promise.all([ + api.get.currentTime(dispose$), + api.get.duration(dispose$), + ]); + return { current, duration }; + }, + }, + }; + return api; + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/common.ts b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/common.ts new file mode 100644 index 0000000000..a6128b1031 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/common.ts @@ -0,0 +1,12 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { + blur: 0, + playing: true, + playingTransition: 0, + opacityTransition: 300, +} as const; +export const D = DEFAULTS; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/mod.ts b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/mod.ts new file mode 100644 index 0000000000..fba2439bc6 --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + */ +export { VimeoBackground } from './ui.tsx'; +export { Vimeo } from './api.Vimeo.ts'; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/t.api.ts b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/t.api.ts new file mode 100644 index 0000000000..1ece4da53e --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/t.api.ts @@ -0,0 +1,14 @@ +import type { t } from './common.ts'; + +/** + * Vimeo simple IFrame/message API. + */ +export type VimeoIFrame = { + post(method: string, value?: number | string): void; + readonly get: { + method<T>(method: string, dispose$?: t.UntilInput): Promise<T>; + time(dispose$?: t.UntilInput): Promise<{ current: t.Secs; duration: t.Secs }>; + currentTime(dispose$?: t.UntilInput): Promise<t.Secs>; + duration(dispose$?: t.UntilInput): Promise<t.Secs>; + }; +}; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/t.ts b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/t.ts new file mode 100644 index 0000000000..3d5ffc46cc --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/t.ts @@ -0,0 +1,32 @@ +import type { t } from './common.ts'; +export type * from './t.api.ts'; + +/** + * <Component>: + */ +export type VimeoBackgroundProps = { + video?: number | t.StringVideoAddress; + + playing?: boolean; + /** + * The delay before the play/stop change takes effect. + * Useful when composing into bigger transitioning baesd compositions. + */ + playingTransition?: t.Msecs; + jumpTo?: t.Secs; + + opacity?: number; + opacityTransition?: t.Msecs; + + blur?: number; + theme?: t.CommonTheme; + style?: t.CssInput; + + onReady?: VimeoReadyHandler; +}; + +/** + * Events + */ +export type VimeoReadyHandler = (e: VimeoReadyHandlerArgs) => void; +export type VimeoReadyHandlerArgs = t.VimeoIFrame; diff --git a/code/sys.ui/ui-react-components/src/ui/VimeoBackground/ui.tsx b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/ui.tsx new file mode 100644 index 0000000000..f5b1432d9d --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/VimeoBackground/ui.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { IFrame } from '../IFrame/mod.ts'; +import { Vimeo } from './api.Vimeo.ts'; +import { type t, Color, DEFAULTS, Time, css, rx } from './common.ts'; + +const D = DEFAULTS; +type P = t.VimeoBackgroundProps; + +export const VimeoBackground: React.FC<P> = (props) => { + const { + opacity, + jumpTo, + blur = D.blur, + opacityTransition = D.opacityTransition, + playingTransition = D.playingTransition, + } = props; + + const [vimeo, setVimeo] = useState<t.VimeoIFrame>(); + const [playing, setPlaying] = useState(props.playing ?? D.playing); + + const [iframe, setIframe] = useState<HTMLIFrameElement>(); + const initialPlaying = useRef(playing); + const src = wrangle.src(props.video, initialPlaying.current); + + /** + * Effect: store API reference when IFrame is ready. + */ + React.useEffect(() => { + if (iframe) { + const vimeo = Vimeo.create(iframe); + setVimeo(vimeo); + props.onReady?.(vimeo); + } + }, [iframe]); + + /** + * Effect: Play/Pause the video via the Vimeo bridge/API. + */ + useEffect(() => { + vimeo?.post(playing ? 'play' : 'pause'); + }, [iframe, playing]); + + /** + * Effect: playing transition. + */ + React.useEffect(() => { + const life = rx.lifecycle(); + const next = props.playing ?? D.playing; + + if (!playingTransition) { + setPlaying(next); // NB: 0 or undefined - immediate change. + } else { + // Make the change after the specified delay. + const time = Time.until(life); + time.delay(playingTransition, () => setPlaying(next)); + } + + return life.dispose; + }, [iframe, props.playing, playing, playingTransition]); + + /** + * Effect: jumpTo (timestamp). + */ + useEffect(() => { + if (jumpTo !== undefined) vimeo?.post('setCurrentTime', jumpTo); + }, [vimeo, iframe, jumpTo]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + position: 'relative', + color: theme.fg, + display: 'grid', + overflow: 'hidden', + opacity: opacity === undefined ? 1 : opacity, + transition: opacityTransition ? `opacity ${opacityTransition}ms` : undefined, + pointerEvents: 'none', + }), + iframe: css({ + userSelect: 'none', + boxSizing: 'border-box', + height: '56.25vw', + left: '50%', + minHeight: '100%', + minWidth: '100%', + transform: 'translate(-50%, -50%)', + position: 'absolute', + top: '50%', + width: '177.77777778vh', + overflow: 'hidden', + border: 'none', + }), + blurMask: css({ + Absolute: 0, + backdropFilter: `blur(${blur}px)`, + opacity: 0.9, + transition: opacityTransition + ? `opacity ${opacityTransition}ms, backdrop-filter ${opacityTransition}ms` + : undefined, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <IFrame + style={styles.iframe} + src={src} + allow={'autoplay; fullscreen'} + allowFullScreen={true} + onReady={(e) => setIframe(e.ref.current ?? undefined)} + /> + <div className={styles.blurMask.class} /> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + src(video: P['video'], playing: P['playing']) { + const id = wrangle.id(video); + const autoplay = playing ? '1' : '0'; + const base = 'https://player.vimeo.com/video'; + return `${base}/${id}?background=1&dnt=true&autoplay=${autoplay}`; + }, + + id(video: P['video']) { + if (typeof video === 'number') return video; + if (typeof video === 'string' && video.startsWith('vimeo/')) { + const [, id] = video.split('/'); + return id; + } + throw new Error(`Failed to parse 'video' prop as a Vimeo ID: ${video}`); + }, +} as const; diff --git a/code/sys.ui/ui-react-components/src/ui/common.ts b/code/sys.ui/ui-react-components/src/ui/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/code/sys.ui/ui-react-components/src/ui/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react-components/vite.config.ts b/code/sys.ui/ui-react-components/vite.config.ts index 25af1be683..dd19226f00 100644 --- a/code/sys.ui/ui-react-components/vite.config.ts +++ b/code/sys.ui/ui-react-components/vite.config.ts @@ -1,14 +1,16 @@ -import { Vite } from '@sys/driver-vite'; -import { defineConfig } from 'vite'; -import { pkg } from './src/pkg.ts'; +import { Vite } from 'jsr:@sys/driver-vite'; +import { defineConfig } from 'npm:vite'; export default defineConfig(() => { - return Vite.Plugin.common({ - pkg, + const entry = './src/-test/index.html'; + const paths = Vite.Config.paths({ app: { entry } }); + return Vite.Config.app({ + paths, chunks(e) { e.chunk('react', 'react'); e.chunk('react.dom', 'react-dom'); e.chunk('sys', ['@sys/std']); + e.chunk('css', ['@sys/ui-css']); }, }); }); diff --git a/code/sys.ui/ui-react-devharness/-scripts/-main.ts b/code/sys.ui/ui-react-devharness/-scripts/-main.ts new file mode 100644 index 0000000000..16e17b93a6 --- /dev/null +++ b/code/sys.ui/ui-react-devharness/-scripts/-main.ts @@ -0,0 +1 @@ +import '../../../sys.driver/driver-vite/src/-entry/-main.ts'; diff --git a/code/sys.ui/ui-react-devharness/-scripts/-tmp.ts b/code/sys.ui/ui-react-devharness/-scripts/-tmp.ts new file mode 100644 index 0000000000..b922a505ad --- /dev/null +++ b/code/sys.ui/ui-react-devharness/-scripts/-tmp.ts @@ -0,0 +1 @@ +console.log('hello temp š'); diff --git a/code/sys.ui/ui-react-devharness/deno.json b/code/sys.ui/ui-react-devharness/deno.json index a0e079c131..d726732068 100644 --- a/code/sys.ui/ui-react-devharness/deno.json +++ b/code/sys.ui/ui-react-devharness/deno.json @@ -1,15 +1,20 @@ { "name": "@sys/ui-react-devharness", - "version": "0.0.82", + "version": "0.0.92", "license": "MIT", "tasks": { + "dev": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=dev --in=./src/-test/index.html", + "build": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=build --in=./src/-test/index.html", + "serve": "deno run -RNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=serve", + "test": "deno test -RWNE --allow-run --allow-ffi", - "lint": "deno lint", - "dry": "deno publish --allow-dirty --dry-run", - "clean": "deno run -RWE ./scripts/-clean.ts", - "dev": "deno run -RWNE --allow-run --allow-ffi ./scripts/-dev.ts", - "build": "deno run -RWNE --allow-run --allow-ffi ./scripts/-build.ts", - "serve": "deno run -RNE --allow-run jsr:@sys/http/server/start" + "dry": "deno publish --allow-dirty --dry-run", + "clean": "deno run -RWE --allow-ffi ./-scripts/-main.ts --cmd=clean", + "upgrade": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=upgrade", + "backup": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=backup", + + "help": "deno run -RE --allow-ffi ./-scripts/-main.ts --cmd=help", + "tmp": "deno run -A ./-scripts/-tmp.ts" }, "exports": { ".": "./src/mod.ts", diff --git a/code/sys.ui/ui-react-devharness/scripts/-build.ts b/code/sys.ui/ui-react-devharness/scripts/-build.ts deleted file mode 100644 index b73d7e663c..0000000000 --- a/code/sys.ui/ui-react-devharness/scripts/-build.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Vite } from '@sys/driver-vite'; -import { pkg } from '@sys/ui-react-devharness'; - -const input = './src/-test/index.html'; -const bundle = await Vite.build({ pkg, input }); -console.info(bundle.toString({ pad: true })); diff --git a/code/sys.ui/ui-react-devharness/scripts/-clean.ts b/code/sys.ui/ui-react-devharness/scripts/-clean.ts deleted file mode 100644 index 68bf3b0c1a..0000000000 --- a/code/sys.ui/ui-react-devharness/scripts/-clean.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Fs } from '@sys/fs'; - -const removeDir = (path: string) => Fs.remove(Fs.resolve(path), { log: true }); -await removeDir('./.tmp'); -await removeDir('./dist'); -await removeDir('./.swc'); diff --git a/code/sys.ui/ui-react-devharness/scripts/-dev.ts b/code/sys.ui/ui-react-devharness/scripts/-dev.ts deleted file mode 100644 index d3ecd700da..0000000000 --- a/code/sys.ui/ui-react-devharness/scripts/-dev.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Run in a child-process (hence the `-allow-run` requirement). - */ -import { Vite } from '@sys/driver-vite'; -import { pkg } from '@sys/ui-react-devharness'; - -const input = './src/-test/index.html'; -const server = await Vite.dev({ pkg, input }); -await server.listen(); diff --git a/code/sys.ui/ui-react-devharness/src/-test/entry.tsx b/code/sys.ui/ui-react-devharness/src/-test/entry.tsx index 2bdcb06472..8790499952 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/entry.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/entry.tsx @@ -1,7 +1,4 @@ -// @ts-types="@types/react" -import React from 'react'; - -import { StrictMode } from 'react'; +import React, { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { pkg } from '../pkg.ts'; @@ -18,25 +15,37 @@ export async function main() { /** * Sample entry logic. */ - const rootElement = document.getElementById('root'); - const root = createRoot(rootElement); + const root = createRoot(document.getElementById('root')!); if (isDev) { /** * NOTE: * The import of the [Dev] module happens dynamically here AFTER * the URL query-string has been interpreted. This allows the base - * module entry to by code-split in such a way that the [Dev Harness] - * never gets sent in the normal useage payload. + * module entry to be code-split in such a way that the [Dev Harness] + * never gets sent in the normal usage payload. */ - const { render } = await import('../mod.ts'); + const { render, useKeyboard } = await import('../mod.ts'); const { ModuleSpecs, SampleSpecs } = await import('./entry.Specs.ts'); + + // Dynamic "runnable specifications" Library: const Specs = { ...SampleSpecs, ...ModuleSpecs, }; + const el = await render(pkg, Specs, { hrDepth: 2, style: { Absolute: 0 } }); - root.render(<StrictMode>{el as React.ReactNode}</StrictMode>); + function App() { + // NB: Any other environment setup here. + useKeyboard(); + return el; + } + + root.render( + <StrictMode> + <App /> + </StrictMode>, + ); } else { const { MySample } = await import('./sample.specs/MySample.tsx'); const el = <MySample style={{ Absolute: 0 }} />; @@ -44,4 +53,4 @@ export async function main() { } } -main().catch((err) => console.error(`Failed to render`, err)); +main().catch((err) => console.error(`Failed to render DevHarness`, err)); diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/-SPEC.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/-SPEC.tsx index a8163d7a7d..443bfd51da 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/-SPEC.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/-SPEC.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Spec } from '../common.ts'; import { DevTools } from './DevTools.tsx'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/DevTools.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/DevTools.tsx index 188fc96ffe..52ee1c75df 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/DevTools.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/DevTools.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Spec, type t } from '../common.ts'; import { ButtonSample } from './ui.Button.tsx'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Button.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Button.tsx index 71084da91a..d2f847768a 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Button.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Button.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { RenderCount } from '../../ui/RenderCount/mod.ts'; import { COLORS, Color, css, useMouse, type t } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Hr.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Hr.tsx index 31c2592e90..1857d57060 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Hr.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.DevTools/ui.Hr.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Color, css } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-1.SPEC.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-1.SPEC.tsx index f394b2e6aa..bceaeddbb5 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-1.SPEC.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-1.SPEC.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Spec } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-2.SPEC.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-2.SPEC.tsx index cab339541c..35b3376454 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-2.SPEC.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs.unit-test/-Sample-2.SPEC.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Spec } from '../common.ts'; import { TestLog } from '../TestLog.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Error.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Error.tsx index b36f8f0416..c55015ac1e 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Error.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Error.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Spec } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.MySample.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.MySample.tsx index 58b5f959c8..f6444d249d 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.MySample.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.MySample.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React, { useState } from 'react'; import { Keyboard } from '@sys/ui-dom'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Size.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Size.tsx index 1c9bc55509..b48c10c4e9 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Size.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/-SPEC.Size.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { type t, css, Spec } from '../common.ts'; import { DevTools } from '../sample.DevTools/mod.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/MySample.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/MySample.tsx index 18ad539c9e..2d504d84f1 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/MySample.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/MySample.tsx @@ -1,6 +1,6 @@ -// @ts-types="@types/react" -import React, { useEffect } from 'react'; -import { Color, css, DEFAULTS, Keyboard, type t } from '../common.ts'; +import React from 'react'; +import { useKeyboard } from '../../ui.use/mod.ts'; +import { type t, Color, css } from '../common.ts'; export type MySampleProps = { text?: string; @@ -14,25 +14,13 @@ let _count = 0; export const MySample: React.FC<MySampleProps> = (props) => { if (props.throwError) { - throw new Error('MySample: Intentional error'); + throw new Error('MySample: š· Intentional error'); } - /** - * [Effects] - */ - useEffect(() => { - const keyboard = Keyboard.on({ - Enter(e) { - const url = new URL(globalThis.location.href); - url.searchParams.set(DEFAULTS.qs.dev, 'true'); - globalThis.location.href = url.href; - }, - }); - return () => keyboard.dispose(); - }, []); + useKeyboard(); /** - * [Render] + * Render: */ const styles = { base: css({ diff --git a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/newFile.tsx b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/newFile.tsx index 63087fc55b..f6b77429a8 100644 --- a/code/sys.ui/ui-react-devharness/src/-test/sample.specs/newFile.tsx +++ b/code/sys.ui/ui-react-devharness/src/-test/sample.specs/newFile.tsx @@ -1,4 +1,4 @@ -import { Spec } from '../../u/Spec/m.Spec'; +import { Spec } from '../mod.ts'; export default Spec.describe('Error on initialize', (e) => { e.it('init', (e) => { diff --git a/code/sys.ui/ui-react-devharness/src/common/libs.ts b/code/sys.ui/ui-react-devharness/src/common/libs.ts index df9b99c59d..567ae326fb 100644 --- a/code/sys.ui/ui-react-devharness/src/common/libs.ts +++ b/code/sys.ui/ui-react-devharness/src/common/libs.ts @@ -3,9 +3,9 @@ export { ErrorBoundary } from 'react-error-boundary'; export { Test } from '@sys/testing/spec'; export { Fuzzy } from '@sys/text/fuzzy'; -export { asArray, Is, maybeWait, Path, R, rx, slug, Time } from '@sys/std'; -export { Str, Value } from '@sys/std/value'; +export { asArray, Is, maybeWait, Obj, Path, R, rx, slug, Time } from '@sys/std'; +export { Lorem, Str, Value } from '@sys/std/value'; export { Color, css, Style } from '@sys/ui-css'; export { Keyboard } from '@sys/ui-dom'; -export { FC, useMouse } from '@sys/ui-react'; +export { FC, Signal, useDist, useMouse } from '@sys/ui-react'; diff --git a/code/sys.ui/ui-react-devharness/src/common/t.ts b/code/sys.ui/ui-react-devharness/src/common/t.ts index 7e75411568..c182fe34c4 100644 --- a/code/sys.ui/ui-react-devharness/src/common/t.ts +++ b/code/sys.ui/ui-react-devharness/src/common/t.ts @@ -7,8 +7,8 @@ export type { IconType } from 'react-icons'; /** * @system */ -export type { ColorConstants } from '@sys/color/t'; -export type { ModuleImport, ModuleImporter, ModuleImports } from '@sys/std/t'; +export type { ColorConstants, ColorTheme } from '@sys/color/t'; +export type { ModuleImport, ModuleImporter, ModuleImports, Signal } from '@sys/std/t'; export type * from '@sys/types/t'; export type { @@ -25,11 +25,11 @@ export type { export type { CssEdgesArray, + CssInput, CssMarginArray, CssMarginInput, CssPaddingArray, CssValue, - CssInput, } from '@sys/ui-css/t'; export type { KeyboardEventsUntil, KeyboardModifierFlags } from '@sys/ui-dom/t'; diff --git a/code/sys.ui/ui-react-devharness/src/m.Dev/-.test.ts b/code/sys.ui/ui-react-devharness/src/m.Dev/-.test.ts new file mode 100644 index 0000000000..a19c8b9a60 --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/m.Dev/-.test.ts @@ -0,0 +1,9 @@ +import { type t, describe, it, expect } from '../-test.ts'; +import { Dev } from './mod.ts'; +import { Theme } from '../u/mod.ts'; + +describe('Dev', () => { + it('API', () => { + expect(Dev.Theme).to.equal(Theme); + }); +}); diff --git a/code/sys.ui/ui-react-devharness/src/m.Dev/m.Dev.ts b/code/sys.ui/ui-react-devharness/src/m.Dev/m.Dev.ts index bd60b388d0..2b8252194e 100644 --- a/code/sys.ui/ui-react-devharness/src/m.Dev/m.Dev.ts +++ b/code/sys.ui/ui-react-devharness/src/m.Dev/m.Dev.ts @@ -2,6 +2,7 @@ import { headless } from '../-test/headless/mod.ts'; import { DevBus as Bus } from '../u/m.Bus/mod.ts'; import { Context } from '../u/m.Ctx/mod.ts'; import { Spec } from '../u/m.Spec/mod.ts'; +import { Theme } from '../u/m.Theme/mod.ts'; import { ValueHandler } from '../u/m.Tools/mod.ts'; import { Harness } from '../ui/Harness/mod.ts'; import { ModuleList } from '../ui/ModuleList/mod.ts'; @@ -15,6 +16,7 @@ export const Dev = { Spec, ModuleList, Harness, + Theme, ValueHandler, headless, } as const; diff --git a/code/sys.ui/ui-react-devharness/src/m.Dev/u.render.tsx b/code/sys.ui/ui-react-devharness/src/m.Dev/u.render.tsx index c13e70c4ba..5f0934c425 100644 --- a/code/sys.ui/ui-react-devharness/src/m.Dev/u.render.tsx +++ b/code/sys.ui/ui-react-devharness/src/m.Dev/u.render.tsx @@ -1,5 +1,5 @@ -// @ts-types="@types/react" import React from 'react'; + import { DevArgs, DevKeyboard } from '../u/mod.ts'; import { Harness } from '../ui/Harness/mod.ts'; import { ModuleList } from '../ui/ModuleList/mod.ts'; @@ -24,7 +24,7 @@ export type Render = ( pkg: { name: string; version: string }, specs: t.SpecImports, options?: RenderOptions, -) => Promise<t.JSXElement>; +) => Promise<React.ReactElement>; /** * Render a harness with the selected `dev=<namespace>` diff --git a/code/sys.ui/ui-react-devharness/src/mod.ts b/code/sys.ui/ui-react-devharness/src/mod.ts index 00b156365e..1a38218d75 100644 --- a/code/sys.ui/ui-react-devharness/src/mod.ts +++ b/code/sys.ui/ui-react-devharness/src/mod.ts @@ -40,8 +40,8 @@ export type * as t from './types.ts'; export { Dev, render } from './m.Dev/mod.ts'; -export { useRubberband } from './ui.use/useRubberband.ts'; +export { useKeyboard, useRubberband } from './ui.use/mod.ts'; export { ModuleList } from './ui/ModuleList/mod.ts'; -export { Badges, COLORS } from './common.ts'; +export { Badges, COLORS, Lorem } from './common.ts'; export { DevArgs, DevKeyboard, Is, Spec, ValueHandler } from './u/mod.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/pkg.ts b/code/sys.ui/ui-react-devharness/src/pkg.ts index 79cb3f9c80..1ba7df8f34 100644 --- a/code/sys.ui/ui-react-devharness/src/pkg.ts +++ b/code/sys.ui/ui-react-devharness/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/ui-react-devharness', version: '0.0.92' }; diff --git a/code/sys.ui/ui-react-devharness/src/t/t.ctx.ts b/code/sys.ui/ui-react-devharness/src/t/t.ctx.ts index 459bbfac57..413267cc8e 100644 --- a/code/sys.ui/ui-react-devharness/src/t/t.ctx.ts +++ b/code/sys.ui/ui-react-devharness/src/t/t.ctx.ts @@ -80,7 +80,7 @@ export type DevCtxSubject = { color(value?: Color | null): DevCtxSubject; backgroundColor(value?: Color | null): DevCtxSubject; size(value: DevSubjectSize): DevCtxSubject; - size(mode: DevFillMode, margin?: t.CssMarginInput): DevCtxSubject; + size(mode?: DevFillMode, margin?: t.CssMarginInput): DevCtxSubject; render<T extends O = O>(fn: t.DevRenderer<T>): DevCtxSubject; }; diff --git a/code/sys.ui/ui-react-devharness/src/t/t.render.props.ts b/code/sys.ui/ui-react-devharness/src/t/t.render.props.ts index 311f31def2..b300a401d3 100644 --- a/code/sys.ui/ui-react-devharness/src/t/t.render.props.ts +++ b/code/sys.ui/ui-react-devharness/src/t/t.render.props.ts @@ -120,7 +120,7 @@ export type DevRenderSizeCenter = { }; /** - * Render size foa an element filling the container. + * Render size for an element filling the container. */ export type DevRenderSizeFill = { mode: 'fill'; diff --git a/code/sys.ui/ui-react-devharness/src/types.ts b/code/sys.ui/ui-react-devharness/src/types.ts index a61795f296..d284faddb1 100644 --- a/code/sys.ui/ui-react-devharness/src/types.ts +++ b/code/sys.ui/ui-react-devharness/src/types.ts @@ -5,5 +5,6 @@ export type * from './t/mod.ts'; export type * from './u/types.ts'; -export type * from './ui.use/types.ts'; + +export type * from './ui.use/t.ts'; export type * from './ui/types.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.Controller.ts b/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.Controller.ts index c814cd8651..c2c070c4a3 100644 --- a/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.Controller.ts +++ b/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.Controller.ts @@ -1,7 +1,7 @@ import { Context } from '../m.Ctx/mod.ts'; import { BusEvents } from './Bus.Events.ts'; import { BusMemoryState } from './Bus.MemoryState.ts'; -import { DEFAULTS, Id, Is, R, Test, rx, type t } from './common.ts'; +import { type t, Obj, DEFAULTS, Id, Is, R, Test, rx } from './common.ts'; /** * Start the controller and return an event API. @@ -172,7 +172,7 @@ export function BusController(args: { } if (typeof e.mutate === 'object') { - draft.render.state = R.clone(e.mutate); // š· TEMP | SLOW (potentially too slow). Not needed when using immutability plugin. + draft.render.state = Obj.clone(e.mutate); // š· TEMP | SLOW (potentially too slow). Not needed when using immutability plugin. } draft.render.revision.state += 1; diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.MemoryState.ts b/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.MemoryState.ts index 9bdf299467..169ebd9536 100644 --- a/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.MemoryState.ts +++ b/code/sys.ui/ui-react-devharness/src/u/m.Bus/Bus.MemoryState.ts @@ -1,4 +1,4 @@ -import { DEFAULTS, Id, Is, R, rx, type t } from './common.ts'; +import { type t, DEFAULTS, Id, Is, Obj, R, rx } from './common.ts'; type Revision = { number: number; message: string }; @@ -40,7 +40,7 @@ export function BusMemoryState(args: { * Make these options available as an injected plugin (IoC). */ const before = api.revision; - const clone = R.clone(_current); // TEMP | SLOW (potentially too slow) š· + const clone = Obj.clone(_current); // TEMP | SLOW (potentially too slow) š· if (typeof change === 'function') { const res = change(clone); diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Ctx/-.test.tsx b/code/sys.ui/ui-react-devharness/src/u/m.Ctx/-.test.tsx index 02ce84cbd8..2829c0478b 100644 --- a/code/sys.ui/ui-react-devharness/src/u/m.Ctx/-.test.tsx +++ b/code/sys.ui/ui-react-devharness/src/u/m.Ctx/-.test.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { TestSample, describe, expect, it, type t } from '../../-test.ts'; import { DEFAULTS, Id } from './common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Ctx/Ctx.Props.Subject.ts b/code/sys.ui/ui-react-devharness/src/u/m.Ctx/Ctx.Props.Subject.ts index 860bee92f5..f275a90752 100644 --- a/code/sys.ui/ui-react-devharness/src/u/m.Ctx/Ctx.Props.Subject.ts +++ b/code/sys.ui/ui-react-devharness/src/u/m.Ctx/Ctx.Props.Subject.ts @@ -17,6 +17,8 @@ export function CtxPropsSubject(props: PropArgs) { const current = props.current().subject; current.size = undefined; + if (args[0] === undefined) return api; + if (Array.isArray(args[0])) { const [width, height] = args[0]; if (Is.nil(width) && Is.nil(height)) { diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Theme/-.test.ts b/code/sys.ui/ui-react-devharness/src/u/m.Theme/-.test.ts new file mode 100644 index 0000000000..e97b81d40f --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/u/m.Theme/-.test.ts @@ -0,0 +1,3 @@ +import { type t, describe, it, expect, Testing } from '../../-test.ts'; + +describe('Dev.Theme', () => {}); diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Theme/common.ts b/code/sys.ui/ui-react-devharness/src/u/m.Theme/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/u/m.Theme/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Theme/mod.ts b/code/sys.ui/ui-react-devharness/src/u/m.Theme/mod.ts new file mode 100644 index 0000000000..1cf29b6ded --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/u/m.Theme/mod.ts @@ -0,0 +1,39 @@ +/** + * @module + */ +import { type t, Color, Signal } from './common.ts'; + +type Color = string | number; +const DEFAULT_THEME: t.CommonTheme = 'Light'; + +/** + * Helpers for working with common themes within the harness. + */ +export const Theme: t.DevThemeLib = { + /** + * Adjust the theme of the DevHarness. + */ + background(ctx, theme, subjectLight, subjectDark) { + const { host, subject } = ctx; + const is = Color.theme(theme ?? DEFAULT_THEME).is; + if (is.light) host.color(null).backgroundColor(null).tracelineColor(null); + if (is.dark) host.color(Color.WHITE).backgroundColor(Color.DARK).tracelineColor(0.08); + + if (!!subjectLight || !!subjectDark) { + if (!!subjectLight) subject.backgroundColor(is.light ? subjectLight : 0); + if (!!subjectDark) subject.backgroundColor(is.dark ? subjectDark : 0); + } + + return Color.theme(theme); + }, + + /** + * Hook into a theme Signal and keep the DevHarness sycned with it. + */ + signalEffect(ctx, theme, subjectLight, subjectDark) { + Signal.effect(() => { + Theme.background(ctx, theme.value, subjectLight, subjectDark); + ctx.redraw(); + }); + }, +}; diff --git a/code/sys.ui/ui-react-devharness/src/u/m.Theme/t.ts b/code/sys.ui/ui-react-devharness/src/u/m.Theme/t.ts new file mode 100644 index 0000000000..f64eda4f42 --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/u/m.Theme/t.ts @@ -0,0 +1,25 @@ +import type { t } from './common.ts'; + +type Color = string | number; +type ThemeInput = t.ColorTheme | t.CommonTheme | null; + +/** + * Helpers for working with common themes within the harness. + */ +export type DevThemeLib = { + /** Adjust the theme of the DevHarness. */ + background( + ctx: t.DevCtx, + theme?: ThemeInput, + subjectLight?: Color | null, + subjectDark?: Color | null, + ): void; + + /** Hook into a theme Signal and keep the DevHarness sycned with it. */ + signalEffect( + ctx: t.DevCtx, + signal: t.Signal, + subjectLight?: Color | null, + subjectDark?: Color | null, + ): void; +}; diff --git a/code/sys.ui/ui-react-devharness/src/u/mod.ts b/code/sys.ui/ui-react-devharness/src/u/mod.ts index c6a579419d..80d939ac7d 100644 --- a/code/sys.ui/ui-react-devharness/src/u/mod.ts +++ b/code/sys.ui/ui-react-devharness/src/u/mod.ts @@ -4,4 +4,5 @@ export { Context } from './m.Ctx/mod.ts'; export { Is } from './m.Is/mod.ts'; export { DevKeyboard } from './m.Keyboard/mod.ts'; export { Spec } from './m.Spec/mod.ts'; +export { Theme } from './m.Theme/mod.ts'; export { ValueHandler } from './m.Tools/mod.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/u/types.ts b/code/sys.ui/ui-react-devharness/src/u/types.ts index f9b4d7273a..c600a344a8 100644 --- a/code/sys.ui/ui-react-devharness/src/u/types.ts +++ b/code/sys.ui/ui-react-devharness/src/u/types.ts @@ -4,4 +4,5 @@ export type * from './m.Ctx/t.ts'; export type * from './m.Is/t.ts'; export type * from './m.Keyboard/t.ts'; export type * from './m.Spec/t.ts'; +export type * from './m.Theme/t.ts'; export type * from './m.Tools/t.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui.use/mod.ts b/code/sys.ui/ui-react-devharness/src/ui.use/mod.ts index 2cc60961f6..fff1a0bdef 100644 --- a/code/sys.ui/ui-react-devharness/src/ui.use/mod.ts +++ b/code/sys.ui/ui-react-devharness/src/ui.use/mod.ts @@ -4,3 +4,4 @@ export * from './useRedrawEvent.ts'; export * from './useRenderer.ts'; export * from './useRubberband.ts'; export * from './useSizeObserver.ts'; +export * from './use.Keyboard.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui.use/t.ts b/code/sys.ui/ui-react-devharness/src/ui.use/t.ts new file mode 100644 index 0000000000..3cb8dca31a --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/ui.use/t.ts @@ -0,0 +1,4 @@ +/** + * Hook: DevHarness Keyboard controller. + */ +export type UseKeyboardFactory = () => void; diff --git a/code/sys.ui/ui-react-devharness/src/ui.use/types.ts b/code/sys.ui/ui-react-devharness/src/ui.use/types.ts deleted file mode 100644 index cb0ff5c3b5..0000000000 --- a/code/sys.ui/ui-react-devharness/src/ui.use/types.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/code/sys.ui/ui-react-devharness/src/ui.use/use.Keyboard.ts b/code/sys.ui/ui-react-devharness/src/ui.use/use.Keyboard.ts new file mode 100644 index 0000000000..70aa758e74 --- /dev/null +++ b/code/sys.ui/ui-react-devharness/src/ui.use/use.Keyboard.ts @@ -0,0 +1,50 @@ +import { useEffect } from 'react'; +import { type t, Keyboard } from './common.ts'; + +/** + * Hook: Keyboard controller. + */ +export const useKeyboard: t.UseKeyboardFactory = () => { + useEffect(() => { + const keyboard = Keyboard.until(); + + const getUrl = () => { + const url = new URL(window.location.href); + const query = url.searchParams; + return { url, query }; + }; + + const is = { + get dev() { + return getUrl().query.has('dev'); + }, + } as const; + + /** + * GOTO: DevHarness. + */ + keyboard.on('CMD + Enter', () => { + if (!is.dev) { + const { url, query } = getUrl(); + query.set('dev', 'true'); + window.location.href = url.href; + } + }); + + /** + * GOTO: Root + */ + keyboard.on('CMD + Escape', () => { + const { url, query } = getUrl(); + + if (is.dev) { + const current = query.get('dev'); + if (current === 'true') query.delete('dev'); // ā goto Root screen. + else query.set('dev', 'true'); // ā goto DevHarness index. + window.location.href = url.href; + } + }); + + return keyboard.dispose; + }, []); +}; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Background.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Background.tsx index d3c2b5c781..89fde2b1f2 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Background.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Background.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, type t } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Component.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Component.tsx index 1082a410a5..c750d87292 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Component.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Component.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Color, css, useRenderer, type t } from '../common.ts'; import { Wrangle } from './u.ts'; @@ -41,7 +40,7 @@ export const HostComponent: React.FC<HostComponentProps> = (props) => { <div ref={props.subjectRef} className={styles.body.class} - data-component={'dev.harness:ComponentHost'} + data-component={'sys.ui.dev.harness:ComponentHost'} > {element as t.ReactNode} </div> diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Grid.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Grid.tsx index f7503215a1..94f09a7adb 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Grid.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Grid.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, type t } from '../common.ts'; import { Wrangle } from './u.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layer.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layer.tsx index 53b6511ab3..dbf472bb64 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layer.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layer.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, useRenderer, type t } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layers.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layers.tsx index 848632b91f..7b5be8c46b 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layers.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.Layers.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, type t } from '../common.ts'; import { HostLayer } from './Host.Layer.tsx'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.tsx index c9e1d1229c..7d3f400ae0 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Host/Host.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React, { useEffect, useState } from 'react'; import { Color, css, DEFAULTS, R, Time, useCurrentState, type t } from '../common.ts'; @@ -41,7 +40,7 @@ export const HarnessHost: React.FC<HarnessHostProps> = (props) => { }, [isEmpty]); /** - * Render + * Render: */ const cropmark = wrangle.cropmark(renderProps); const backgroundColor = @@ -57,7 +56,7 @@ export const HarnessHost: React.FC<HarnessHostProps> = (props) => { const styles = { base: css({ position: 'relative', backgroundColor, color }), body: css({ Absolute: 0, display: 'grid', gridTemplateRows: 'auto 1fr auto' }), - main: css({ position: 'relative', display: 'grid' }), + main: css({ position: 'relative', display: 'grid', overflow: 'hidden' }), empty: css({ Absolute: 0, display: 'grid', placeContent: 'center', userSelect: 'none' }), }; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.Row.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.Row.tsx index 38d60fc39f..916abbc8cc 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.Row.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.Row.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, useRenderer, type t } from '../common.ts'; @@ -15,6 +14,7 @@ export const DebugPanelBodyRow: React.FC<DebugPanelBodyRow> = (props) => { base: css({ position: 'relative', display: 'grid', + lineHeight: 1.6, }), }; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.tsx index fd58e1401e..5072fd950c 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.Body.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, type t } from '../common.ts'; import { DebugPanelBodyRow as Row } from './Panel.Body.Row.tsx'; @@ -13,7 +12,9 @@ export const DebugPanelBody: React.FC<DebugPanelBodyrops> = (props) => { const { instance, current } = props; const renderers = current?.render?.props?.debug.body.renderers ?? []; - const styles = { base: css({ position: 'relative' }) }; + const styles = { + base: css({ position: 'relative' }), + }; const elements = renderers.filter(Boolean).map((renderer) => { return <Row key={renderer.id} instance={instance} renderer={renderer} />; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.tsx index d6a3bb56bd..951ff308b1 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Debug/Panel.tsx @@ -1,17 +1,20 @@ -// @ts-types="@types/react" import React from 'react'; -import { Color, css, R, useCurrentState, type t } from '../common.ts'; +import { type t, Style, Color, css, R, useCurrentState } from '../common.ts'; import { PanelFooter, PanelHeader } from '../Harness.Panel.Edge/mod.ts'; import { DebugPanelBody as Body } from './Panel.Body.tsx'; export type DebugPanelProps = { instance: t.DevInstance; baseRef?: React.RefObject<HTMLDivElement>; + theme?: t.CommonTheme; style?: t.CssInput; }; +const componentAttr = 'sys.ui.dev.harness:Panel.Debug'; + export const DebugPanel: React.FC<DebugPanelProps> = (props) => { const { instance } = props; + const theme = Color.theme(props.theme); const current = useCurrentState(instance, { distinctUntil }); const debug = current.info?.render.props?.debug; @@ -20,6 +23,18 @@ export const DebugPanel: React.FC<DebugPanelProps> = (props) => { if (width === null) return null; // NB: configured to not render. const isHidden = width <= 0; + /** + * Common styling for children. + */ + React.useEffect(() => { + const sheet = Style.Dom.stylesheet(); + sheet.rule(`[data-component="${componentAttr}"] hr`, { + border: 'none', + borderTop: '1px solid', + borderColor: Color.alpha(theme.fg, 0.2), + }); + }, []); + /** * [Render] */ @@ -27,8 +42,8 @@ export const DebugPanel: React.FC<DebugPanelProps> = (props) => { base: css({ overflow: 'hidden', justifySelf: 'stretch', - borderLeft: `solid 1px ${Color.format(-0.1)}`, width, + borderLeft: `solid 1px ${Color.format(-0.1)}`, display: isHidden ? 'none' : 'grid', gridTemplateRows: 'auto 1fr auto', pointerEvents: isHidden ? 'none' : undefined, @@ -42,7 +57,7 @@ export const DebugPanel: React.FC<DebugPanelProps> = (props) => { return ( <div ref={props.baseRef} - data-component={'dev.harness:Panel.Debug'} + data-component={componentAttr} className={css(styles.base, props.style).class} > <PanelHeader instance={instance} current={debug?.header} /> diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Edge/Panel.Edge.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Edge/Panel.Edge.tsx index 9f6e4667ee..1bb2616b9f 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Edge/Panel.Edge.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness.Panel.Edge/Panel.Edge.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { css, useRenderer, type t } from '../common.ts'; import { Wrangle } from './u.ts'; @@ -18,7 +17,7 @@ export const PanelEdge: React.FC<PanelBarProps> = (props) => { if (!renderer || !element) return <div />; /** - * Render + * Render: */ const border = (hasBorder: boolean) => (hasBorder ? Wrangle.borderStyle(current) : undefined); const styles = { diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness/-SPEC.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness/-SPEC.tsx index 1db1a9bfee..f7a2cc367f 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness/-SPEC.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness/-SPEC.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Spec } from '../../-test.ts'; import { Harness } from './mod.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness/ErrorFallback.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness/ErrorFallback.tsx index 47fd65138c..16a8636293 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness/ErrorFallback.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness/ErrorFallback.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import type { FallbackProps } from 'react-error-boundary'; import { COLORS, css, type t } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Harness/Harness.tsx b/code/sys.ui/ui-react-devharness/src/ui/Harness/Harness.tsx index 0f55e13ace..387d939c51 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Harness/Harness.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Harness/Harness.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React, { useEffect, useRef, type FC } from 'react'; import { HarnessHost } from '../Harness.Host/mod.ts'; import { DebugPanel } from '../Harness.Panel.Debug/mod.ts'; @@ -73,7 +72,7 @@ export const Harness: FC<t.HarnessProps> = (props: t.HarnessProps) => { return ( <div - data-component={'dev.harness'} + data-component={'sys.ui.dev.harness'} ref={baseRef} className={css(styles.reset, styles.base, props.style).class} > diff --git a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/-SPEC.tsx b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/-SPEC.tsx index fb0e7c0213..babea86c00 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/-SPEC.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/-SPEC.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Badges, COLORS, pkg, Spec, type t } from '../../-test.ts'; import { ModuleList } from './mod.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Footer.tsx b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Footer.tsx index 414f59e88e..a0f0096275 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Footer.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Footer.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { Color, css, type t } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.Item.tsx b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.Item.tsx index 8f2a0b3cd9..d6e3b830c5 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.Item.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.Item.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React, { useEffect, useRef } from 'react'; import { VscSymbolClass } from 'react-icons/vsc'; import { COLORS, Calc, Color, DEFAULTS, css, type t } from './common.ts'; @@ -68,7 +67,7 @@ export const ListItem: React.FC<ListItemProps> = (props) => { }; /** - * Render + * Render: */ const { WHITE, BLUE } = COLORS; const color = Color.theme(props.theme).fg; diff --git a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.tsx b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.tsx index 3c486388ef..e9c754b296 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.List.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { VscSymbolClass } from 'react-icons/vsc'; import { Color, css, DEFAULTS, type t } from './common.ts'; @@ -29,7 +28,7 @@ export const List: React.FC<ListProps> = (props) => { const hasDevParam = url.searchParams.has(DEFAULTS.qs.dev); /** - * Render + * Render: */ const color = Color.theme(theme).fg; const styles = { diff --git a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Title.tsx b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Title.tsx index 2548855529..05cb3e766f 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Title.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.Title.tsx @@ -1,44 +1,67 @@ import React from 'react'; -import { Color, css, type t } from '../common.ts'; +import { type t, Color, css, Str } from '../common.ts'; export type TitleProps = { enabled?: boolean; title?: string; version?: string; + dist?: t.DistPkg; badge?: t.ImageBadge; theme?: t.CommonTheme; style?: t.CssInput; }; export const Title: React.FC<TitleProps> = (props) => { + const { dist } = props; const title = props.title?.trim(); const badge = props.badge; if (!(title || badge)) return null; /** - * Render + * Render: */ - const color = Color.theme(props.theme).fg; + const theme = Color.theme(props.theme); + const color = theme.fg; const styles = { base: css({ display: 'grid', gridTemplateColumns: `1fr auto`, color }), left: css({ fontWeight: 'bold' }), right: css({ display: 'grid', alignContent: 'center' }), block: css({ display: 'block' }), + + title: css({ display: 'grid', gridTemplateColumns: `auto 1fr auto` }), version: css({ color: Color.alpha(color, 0.3), marginLeft: 3 }), + hash: css({ marginLeft: 10, fontWeight: 'normal' }), + link: css({ + color: Color.BLUE, + textDecoration: 'none', + ':hover': { textDecoration: 'underline' }, + }), }; + const linkProps = { target: '_blank', rel: 'noopener noreferrer' }; + const elBadge = badge && ( - <a href={badge?.href} target={'_blank'} rel={'noopener noreferrer'}> + <a href={badge?.href} {...linkProps}> <img className={styles.block.class} src={badge?.image} /> </a> ); + const elDist = dist && ( + <a className={css(styles.hash, styles.link).class} href={'./dist.json'} {...linkProps}> + {`${Str.bytes(dist.size.bytes)} ā dist.pkg:#${dist.hash.digest.slice(-5)}`} + </a> + ); + const elTitle = title && ( - <> - <span>{title}</span> - {props.version && <span className={styles.version.class}>{`@${props.version}`}</span>} - </> + <div className={styles.title.class}> + <div> + <span>{title}</span> + {props.version && <span className={styles.version.class}>{`@${props.version}`}</span>} + </div> + <div /> + <div>{elDist}</div> + </div> ); return ( diff --git a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.tsx b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.tsx index 7dca1b19a7..0efb35d66a 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/ModuleList/ui.tsx @@ -1,8 +1,7 @@ -// @ts-types="@types/react" import React from 'react'; import { useEffect, useRef } from 'react'; -import { Color, DEFAULTS, css, useRubberband, type t } from './common.ts'; +import { type t, useDist, Color, DEFAULTS, css, useRubberband } from './common.ts'; import { Footer } from './ui.Footer.tsx'; import { List } from './ui.List.tsx'; import { Title } from './ui.Title.tsx'; @@ -19,6 +18,7 @@ export const View: React.FC<t.ModuleListProps> = (props) => { const baseRef = useRef<HTMLDivElement>(null); const itemRefs = useRef<LiMap>(new Map<number, HTMLLIElement>()); + const dist = useDist({ useSampleFallback: true }); useRubberband(props.allowRubberband ?? false); useScrollObserver(baseRef, itemRefs.current, props.onItemVisibility); useScrollController(baseRef, itemRefs.current, props.scrollTo$); @@ -43,7 +43,7 @@ export const View: React.FC<t.ModuleListProps> = (props) => { }; /** - * Render + * Render: */ const color = Color.theme(theme).fg; const styles = { @@ -95,6 +95,7 @@ export const View: React.FC<t.ModuleListProps> = (props) => { <div className={styles.body.class}> <Title enabled={enabled} + dist={dist?.json} title={props.title} version={props.version} badge={props.badge} diff --git a/code/sys.ui/ui-react-devharness/src/ui/RenderCount/RenderCount.tsx b/code/sys.ui/ui-react-devharness/src/ui/RenderCount/RenderCount.tsx index 8f82fe7823..39adf6e878 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/RenderCount/RenderCount.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/RenderCount/RenderCount.tsx @@ -1,4 +1,3 @@ -// @ts-types="@types/react" import React from 'react'; import { COLORS, css, type t } from '../common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Spinners/BarSpinner.tsx b/code/sys.ui/ui-react-devharness/src/ui/Spinners/BarSpinner.tsx index 965b3140e2..6af071dda5 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Spinners/BarSpinner.tsx +++ b/code/sys.ui/ui-react-devharness/src/ui/Spinners/BarSpinner.tsx @@ -1,8 +1,8 @@ -// @ts-types="@types/react" import React from 'react'; import { COLORS, css, type t } from '../common.ts'; export type BarSpinnerProps = { style?: t.CssInput }; + type BarLoaderProps = { color?: string; width?: number }; let BarLoader: React.ComponentType<BarLoaderProps> | undefined; diff --git a/code/sys.ui/ui-react-devharness/src/ui/Spinners/mod.ts b/code/sys.ui/ui-react-devharness/src/ui/Spinners/mod.ts index 2c1dd04e4d..8e47f3b493 100644 --- a/code/sys.ui/ui-react-devharness/src/ui/Spinners/mod.ts +++ b/code/sys.ui/ui-react-devharness/src/ui/Spinners/mod.ts @@ -1 +1,5 @@ +/** + * @module + * Spinners + */ export * from './BarSpinner.tsx'; diff --git a/code/sys.ui/ui-react/deno.json b/code/sys.ui/ui-react/deno.json index 4349b3c91b..47a22958c0 100644 --- a/code/sys.ui/ui-react/deno.json +++ b/code/sys.ui/ui-react/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/ui-react", - "version": "0.0.84", + "version": "0.0.94", "license": "MIT", "tasks": { "test": "deno test -RW", @@ -13,6 +13,7 @@ "./t": "./src/types.ts", "./types": "./src/types.ts", "./fc": "./src/m.FC/mod.ts", + "./testing/server": "./src/m.Testing.Server/mod.ts", "./use": "./src/m.use/mod.ts" } } diff --git a/code/sys.ui/ui-react/src/-test.ts b/code/sys.ui/ui-react/src/-test.ts index f9f98a3ab9..70890c4c87 100644 --- a/code/sys.ui/ui-react/src/-test.ts +++ b/code/sys.ui/ui-react/src/-test.ts @@ -1,2 +1,2 @@ -export { Testing, describe, expect, it } from '@sys/testing/server'; +export { describe, DomMock, expect, it, Testing } from '@sys/testing/server'; export * from './common.ts'; diff --git a/code/sys.ui/ui-react/src/common/libs.ts b/code/sys.ui/ui-react/src/common/libs.ts index 8f225eff9f..3abb87f746 100644 --- a/code/sys.ui/ui-react/src/common/libs.ts +++ b/code/sys.ui/ui-react/src/common/libs.ts @@ -1 +1,2 @@ -export { Time } from '@sys/std'; +export { Err, rx, Time } from '@sys/std'; +export { css } from '@sys/ui-css'; diff --git a/code/sys.ui/ui-react/src/common/t.ts b/code/sys.ui/ui-react/src/common/t.ts index 27116ce081..4162e85e3b 100644 --- a/code/sys.ui/ui-react/src/common/t.ts +++ b/code/sys.ui/ui-react/src/common/t.ts @@ -1,4 +1,7 @@ -export type { Disposable, Lifecycle, Point, UntilObservable } from '@sys/types'; +export type { ReactNode } from 'react'; + +export type { CssEdgesArray, CssEdgesInput, CssInput } from '@sys/ui-css/t'; export type { KeyboardModifierFlags } from '@sys/ui-dom/t'; +export type * from '@sys/types'; export type * from '../types.ts'; diff --git a/code/sys.ui/ui-react/src/m.Signal/-.test.tsx b/code/sys.ui/ui-react/src/m.Signal/-.test.tsx new file mode 100644 index 0000000000..4d65b41321 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Signal/-.test.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; + +import { describe, expect, it, Testing, Time } from '../-test.ts'; +import { TestReact } from '../m.Testing.Server/mod.ts'; +import { Signal } from './mod.ts'; + +import * as Preact from '@preact/signals-react'; + +/** + * See [@sys/std/signal] unit tests for basic API + * usage sceanrios: + * + * ⢠signal ā (update) + * ⢠effect ā (listen and react to change) + * ⢠computed ā (compound signal value) + * ⢠batch ā (multiple changes, single fire of effect listeners) + * + */ +describe('Signals', { sanitizeOps: false, sanitizeResources: false }, () => { + it('API', () => { + expect(Signal.create).to.equal(Preact.signal); + expect(Signal.effect).to.equal(Preact.effect); + expect(Signal.useSignal).to.equal(Preact.useSignal); + expect(Signal.useEffect).to.equal(Preact.useSignalEffect); + }); + + describe('React hooks', () => { + it('useSignal | useSignalEffect', async () => { + let fired: number[] = []; + + /** + * Render: into a DOM container. + */ + function TestComponent() { + const count = Signal.useSignal(0); + + const [, setRender] = useState(0); + const redraw = () => setRender((n) => n + 1); + + // Runs whenever `count.value` changes. + Signal.useEffect(() => { + fired.push(count.value); + redraw(); + }); + + const increment = () => (count.value += 1); + return ( + <div> + <button onClick={increment}>Increment</button> + <span>{count.value}</span> + </div> + ); + } + + const dom = await TestReact.render(<TestComponent />); + await Time.wait(10); + + /** + * Verify initial state. + */ + const button = dom.container.querySelector('button')!; + const span = dom.container.querySelector('span')!; + expect(span.textContent).to.eql('0'); + expect(fired).to.eql([0, 0]); // NB: initial render(s). + + /** + * Simulate a user click, then wait a microtask so React re-renders. + */ + fired = []; // ā (clear before test). + button.click(); + button.click(); + button.click(); + await Testing.wait(); + + expect(fired).to.eql([1, 2, 3]); + expect(span.textContent).to.equal('3'); + + dom.dispose(); + }); + }); +}); diff --git a/code/sys.ui/ui-react/src/m.Signal/common.ts b/code/sys.ui/ui-react/src/m.Signal/common.ts new file mode 100644 index 0000000000..4feaf9291e --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Signal/common.ts @@ -0,0 +1,3 @@ +export { useSignal, useSignalEffect } from '@preact/signals-react'; +export { Signal as Std } from '@sys/std/signal'; +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react/src/m.Signal/m.Signal.ts b/code/sys.ui/ui-react/src/m.Signal/m.Signal.ts new file mode 100644 index 0000000000..1b06a6a884 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Signal/m.Signal.ts @@ -0,0 +1,16 @@ +import { type t, Std, useSignalEffect as useEffect, useSignal } from './common.ts'; +import { useRedrawEffect } from './u.useRedrawEffect.ts'; + +/** + * Reactive Signals. + * See: + * https://github.com/tc39/proposal-signals + * https://preactjs.com/blog/introducing-signals/ + * https://preactjs.com/guide/v10/signals + */ +export const Signal: t.SignalReactLib = { + ...Std, + useSignal, + useEffect, + useRedrawEffect, +} as const; diff --git a/code/sys.ui/ui-react/src/m.Signal/mod.ts b/code/sys.ui/ui-react/src/m.Signal/mod.ts new file mode 100644 index 0000000000..679b40ce75 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Signal/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Reactive Signals (via "preact/signals") + */ +export { Signal } from './m.Signal.ts'; diff --git a/code/sys.ui/ui-react/src/m.Signal/t.ts b/code/sys.ui/ui-react/src/m.Signal/t.ts new file mode 100644 index 0000000000..c41abbc420 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Signal/t.ts @@ -0,0 +1,26 @@ +import type Preact from '@preact/signals-react'; +import type { SignalLib } from '@sys/std/t'; + +export type { ExtractSignalValue, ReadonlySignal, Signal } from '@sys/std/t'; + +/** + * Reactive Signals. + * See: + * https://github.com/tc39/proposal-signals + * https://preactjs.com/blog/introducing-signals/ + * https://preactjs.com/guide/v10/signals + */ +export type SignalReactLib = SignalLib & { + useSignal: typeof Preact.useSignal; + useEffect: typeof Preact.useSignalEffect; + + /** + * Safely causes a redraw (via a useState counter incrementing) + * when any of the signals that are hooked into within the + * callback change value. + * + * Safe: == stops effect listeners on tear-down. + * + */ + useRedrawEffect(cb: () => void): void; +}; diff --git a/code/sys.ui/ui-react/src/m.Signal/u.useRedrawEffect.ts b/code/sys.ui/ui-react/src/m.Signal/u.useRedrawEffect.ts new file mode 100644 index 0000000000..1f6be3f960 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Signal/u.useRedrawEffect.ts @@ -0,0 +1,11 @@ +import { useState } from 'react'; +import { type t, Time, useSignalEffect } from './common.ts'; + +export const useRedrawEffect: t.SignalReactLib['useRedrawEffect'] = (cb) => { + const [, setRender] = useState(0); + const redraw = () => setRender((n) => n + 1); + useSignalEffect(() => { + cb(); + Time.delay(redraw); + }); +}; diff --git a/code/sys.ui/ui-react/src/m.Testing.Server/-.test.tsx b/code/sys.ui/ui-react/src/m.Testing.Server/-.test.tsx new file mode 100644 index 0000000000..c5c8cfd214 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Testing.Server/-.test.tsx @@ -0,0 +1,38 @@ +import { describe, expect, it } from '../-test.ts'; +import { TestReact } from './mod.ts'; + +describe('TestReact (Server)', { sanitizeOps: false, sanitizeResources: false }, () => { + describe('render', () => { + it('renders into DOM', async () => { + const el = ( + <div> + <span>Hello</span> + </div> + ); + + const res = await TestReact.render(el); + const span = res.container.querySelector('span')!; + expect(span.innerText).to.eql('Hello'); + }); + + it('lifecycle', async () => { + const res = await TestReact.render( + <div> + <span>Hello</span> + </div>, + ); + let count = 0; + res.dispose$.subscribe(() => count++); + + expect(res.disposed).to.eql(false); + expect(res.container.querySelector('span')!.innerText).to.eql('Hello'); + + res.dispose(); + expect(res.disposed).to.eql(true); + expect(count).to.eql(1); + + // NB: should not find. + expect(res.container.querySelector('span')).to.eql(null); + }); + }); +}); diff --git a/code/sys.ui/ui-react/src/m.Testing.Server/common.ts b/code/sys.ui/ui-react/src/m.Testing.Server/common.ts new file mode 100644 index 0000000000..26a269aec8 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Testing.Server/common.ts @@ -0,0 +1,2 @@ +export { DomMock, Testing } from '@sys/testing/server'; +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react/src/m.Testing.Server/m.TestReact.ts b/code/sys.ui/ui-react/src/m.Testing.Server/m.TestReact.ts new file mode 100644 index 0000000000..f1ae475076 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Testing.Server/m.TestReact.ts @@ -0,0 +1,9 @@ +import { type t } from './common.ts'; +import { render } from './u.render.tsx'; + +/** + * Renders an element into the service-side test DOM. + */ +export const TestReact: t.TestReactServerLib = { + render, +}; diff --git a/code/sys.ui/ui-react/src/m.Testing.Server/mod.ts b/code/sys.ui/ui-react/src/m.Testing.Server/mod.ts new file mode 100644 index 0000000000..7d69c716ae --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Testing.Server/mod.ts @@ -0,0 +1,6 @@ +/** + * @module + * Tools for testing React on the server. + */ +export { DomMock, Testing } from './common.ts'; +export { TestReact } from './m.TestReact.ts'; diff --git a/code/sys.ui/ui-react/src/m.Testing.Server/t.ts b/code/sys.ui/ui-react/src/m.Testing.Server/t.ts new file mode 100644 index 0000000000..a72b9258a9 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Testing.Server/t.ts @@ -0,0 +1,23 @@ +import type React from 'react'; +import type { t } from './common.ts'; + +/** + * Tools for testing React on the server. + */ +export type TestReactServerLib = { + /** Renders an element into the service-side test DOM. */ + render(el: React.ReactNode, options?: TestReactRenderOptions): Promise<t.TestReactRendered>; +}; + +/** Options passed to the `TestReact.render` method. */ +export type TestReactRenderOptions = { + /** Flag indicating if the element should be contained within the <StrictMode> render container. */ + strict?: boolean; +}; + +/** + * A server-rendered react component. + */ +export type TestReactRendered = t.Lifecycle & { + readonly container: HTMLDivElement; +}; diff --git a/code/sys.ui/ui-react/src/m.Testing.Server/u.render.tsx b/code/sys.ui/ui-react/src/m.Testing.Server/u.render.tsx new file mode 100644 index 0000000000..61211d6bdc --- /dev/null +++ b/code/sys.ui/ui-react/src/m.Testing.Server/u.render.tsx @@ -0,0 +1,40 @@ +import React, { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { type t, DomMock, rx, Time } from './common.ts'; + +/** + * Renders an element into the service-side test DOM. + */ +export const render: t.TestReactServerLib['render'] = async (el, options = {}) => { + const { strict = true } = options; + DomMock.polyfill(); + + const life = rx.lifecycle(); + life.dispose$.subscribe(() => root.unmount()); + + // Construct the server-side DOM. + const container = document.createElement('div'); + document.body.appendChild(container); + const root = createRoot(container); + + // Render into DOM. + if (strict) el = <StrictMode>{el}</StrictMode>; + root.render(el); + await Time.wait(0); // NB: ensures the render completes before returning. + + /** + * API + */ + const api: t.TestReactRendered = { + container, + + // Lifecycle. + dispose: life.dispose, + dispose$: life.dispose$, + get disposed() { + return life.disposed; + }, + }; + return api; +}; diff --git a/code/sys.ui/ui-react/src/m.use/common.ts b/code/sys.ui/ui-react/src/m.use/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/code/sys.ui/ui-react/src/m.use/mod.ts b/code/sys.ui/ui-react/src/m.use/mod.ts index 066fc8fac4..0a30d25897 100644 --- a/code/sys.ui/ui-react/src/m.use/mod.ts +++ b/code/sys.ui/ui-react/src/m.use/mod.ts @@ -10,11 +10,13 @@ * useMouseDrag, * useClickInside, * useClickOutside, + * useSizeObserver, * } from '@sys/ui-react/use'; * ``` - * */ - -export * from './useClick.ts'; -export * from './useMouse.Drag.ts'; -export * from './useMouse.ts'; +export * from './use.Click.ts'; +export * from './use.Dist.ts'; +export * from './use.Is.TouchSupported.ts'; +export * from './use.Mouse.Drag.ts'; +export * from './use.Mouse.ts'; +export * from './use.SizeObserver.tsx'; diff --git a/code/sys.ui/ui-react/src/m.use/t.ts b/code/sys.ui/ui-react/src/m.use/t.ts index dba04b77ce..849f92b713 100644 --- a/code/sys.ui/ui-react/src/m.use/t.ts +++ b/code/sys.ui/ui-react/src/m.use/t.ts @@ -1,126 +1,6 @@ -import type { RefObject, MouseEventHandler } from 'react'; -import type { t } from '../common.ts'; - -type E = HTMLElement; -type Div = HTMLDivElement; -type M = MouseEventHandler; -type MouseCallback = (e: MouseEvent) => void; - -/** - * Properties passed to the `useMouse` hook. - */ -export type UseMouseProps = { - onDown?: M; - onUp?: M; - onEnter?: M; - onLeave?: M; - onDrag?: UseMouseDragHandler; -}; - -/** - * Hook: keep track of mouse events for an HTML element - */ -export type UseMouseHook = (props?: t.UseMouseProps) => t.UseMouse; - -/** - * Hook: keep track of mouse events for an HTML element. - * Usage: - * - * const mouse = useMouse(); - * <div {...mouse.handlers} /> - */ -export type UseMouse = { - readonly is: { readonly over: boolean; readonly down: boolean; readonly dragging: boolean }; - readonly handlers: { onMouseDown: M; onMouseUp: M; onMouseEnter: M; onMouseLeave: M }; - readonly drag?: t.MouseMovement; - reset(): void; -}; - -/** - * Hook: keep track of a mouse drag operation. - */ -export type UseMouseDragHook = (props?: t.UseMouseDragProps) => t.UseMouseDrag; - -/** - * Properties passed to the `useMouseDrag` hook. - */ -export type UseMouseDragProps = { onDrag?: t.UseMouseDragHandler }; - -/** - * Hook: information about a mouse drag operation. - */ -export type UseMouseDrag = { - readonly is: { readonly dragging: boolean }; - readonly enabled: boolean; - readonly movement?: t.MouseMovement; - start(): void; - cancel(): void; -}; - -/** - * Hook: information about the movement of a mouse cursor. - */ -export type MouseMovement = { - readonly x: number; - readonly y: number; - readonly movement: t.Point; - readonly client: t.Point; - readonly page: t.Point; - readonly offset: t.Point; - readonly screen: t.Point; - readonly button: number; - readonly modifiers: t.KeyboardModifierFlags; -}; - -/** - * Event handler for a mouse-drag operation. - */ -export type UseMouseDragHandler = (e: UseMouseDragHandlerArgs) => void; - -/** - * Agrument supplied to the mouse-drag handler. - */ -export type UseMouseDragHandlerArgs = MouseMovement & { cancel(): void }; - -/** - * Hook: information about a mouse click operations - */ -export type UseClickHook<T extends E = Div> = (input: t.UseClickInput<T>) => UseClick<T>; - -/** - * Hook: Monitors for click events outside the given element. - * Usage: - * Useful for clicking away from modal dialogs or popups. - */ -export type UseClickOutsideHook<T extends E = Div> = UseClickHook<T>; - -/** - * Hook: Monitors for click events within the given element. - * Usage: - * Useful for clicking away from modal dialogs or popups. - */ -export type UseClickInsideHook<T extends E = Div> = UseClickHook<T>; - -/** - * Input passed to the UseClick hook. - */ -export type UseClickInput<T extends E> = t.UseClickProps<T> | MouseCallback; - -/** - * Hook: information about a mouse click operations - */ -export type UseClick<T extends E> = { - readonly ref: RefObject<T>; - readonly stage: t.UseClickStage; -}; - -/** - * Properties passed to the `UseClick` hook. - */ -export type UseClickProps<T extends E> = { - stage?: t.UseClickStage; - ref?: RefObject<T>; - callback?: MouseCallback; -}; - -export type UseClickStage = 'down' | 'up'; +export type * from './t.use.Click.ts'; +export type * from './t.use.Dist.ts'; +export type * from './t.use.Is.ts'; +export type * from './t.use.Mouse.Drag.ts'; +export type * from './t.use.Mouse.ts'; +export type * from './t.use.SizeObserver.ts'; diff --git a/code/sys.ui/ui-react/src/m.use/t.use.Click.ts b/code/sys.ui/ui-react/src/m.use/t.use.Click.ts new file mode 100644 index 0000000000..c88005346a --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/t.use.Click.ts @@ -0,0 +1,31 @@ +import type { RefObject } from 'react'; +import type { t } from './common.ts'; + +type E = HTMLElement; +type Div = HTMLDivElement; +type MouseCallback = (e: MouseEvent) => void; + +/** + * Hook: information about a mouse click operations + */ +export type UseClickHook<T extends E = Div> = (input: t.UseClickInput<T>) => ClickHook<T>; +/** Input passed to the UseClick hook. */ +export type UseClickInput<T extends E> = t.ClickHookProps<T> | MouseCallback; + +/** + * Hook: information about a mouse click operations + */ +export type ClickHook<T extends E> = { + readonly ref: RefObject<T>; + readonly stage: t.UseClickStage; +}; + +/** + * Properties passed to the `UseClick` hook. + */ +export type ClickHookProps<T extends E> = { + stage?: t.UseClickStage; + ref?: RefObject<T>; + callback?: MouseCallback; +}; +export type UseClickStage = 'down' | 'up'; diff --git a/code/sys.ui/ui-react/src/m.use/t.use.Dist.ts b/code/sys.ui/ui-react/src/m.use/t.use.Dist.ts new file mode 100644 index 0000000000..b5ce92e475 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/t.use.Dist.ts @@ -0,0 +1,12 @@ +import type { t } from './common.ts'; + +/** + * Hook: Load the `dist.json` file from the server (if avilable). + */ +export type UseDistFactory = (options?: { useSampleFallback?: boolean }) => UseDist; +export type UseDist = { + readonly count: number; + readonly is: { readonly sample: boolean }; + readonly json?: t.DistPkg; + readonly error?: t.StdError; +}; diff --git a/code/sys.ui/ui-react/src/m.use/t.use.Is.ts b/code/sys.ui/ui-react/src/m.use/t.use.Is.ts new file mode 100644 index 0000000000..c5aa7e08af --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/t.use.Is.ts @@ -0,0 +1,4 @@ +/** + * Hook: detect if the device supports touch events. + */ +export type UseIsTouchSupported = () => boolean; diff --git a/code/sys.ui/ui-react/src/m.use/t.use.Mouse.Drag.ts b/code/sys.ui/ui-react/src/m.use/t.use.Mouse.Drag.ts new file mode 100644 index 0000000000..d82b29adbb --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/t.use.Mouse.Drag.ts @@ -0,0 +1,47 @@ +import type { t } from './common.ts'; + +/** + * Hook Factory: keep track of a mouse drag operation. + */ +export type UseMouseDrag = (props?: t.MouseDragHookProps) => t.MouseDragHook; + +/** + * Properties passed to the `useMouseDrag` hook. + */ +export type MouseDragHookProps = { onDrag?: t.UseMouseDragHandler }; + +/** + * Hook: information about a mouse drag operation. + */ +export type MouseDragHook = { + readonly is: { readonly dragging: boolean }; + readonly enabled: boolean; + readonly movement?: t.MouseMovement; + start(): void; + cancel(): void; +}; + +/** + * Hook: information about the movement of a mouse cursor. + */ +export type MouseMovement = { + readonly x: number; + readonly y: number; + readonly movement: t.Point; + readonly client: t.Point; + readonly page: t.Point; + readonly offset: t.Point; + readonly screen: t.Point; + readonly button: number; + readonly modifiers: t.KeyboardModifierFlags; +}; + +/** + * Event handler for a mouse-drag operation. + */ +export type UseMouseDragHandler = (e: UseMouseDragHandlerArgs) => void; + +/** + * Agrument supplied to the mouse-drag handler. + */ +export type UseMouseDragHandlerArgs = MouseMovement & { cancel(): void }; diff --git a/code/sys.ui/ui-react/src/m.use/t.use.Mouse.ts b/code/sys.ui/ui-react/src/m.use/t.use.Mouse.ts new file mode 100644 index 0000000000..0cd90ab3e8 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/t.use.Mouse.ts @@ -0,0 +1,34 @@ +import type { MouseEventHandler } from 'react'; +import type { t } from './common.ts'; + +type M = MouseEventHandler; + +/** + * Hook Factory: keep track of mouse events for an HTML element + */ +export type UseMouse = (props?: t.MouseHookProps) => t.MouseHook; + +/** + * Hook: keep track of mouse events for an HTML element. + * Usage: + * + * const mouse = useMouse(); + * <div {...mouse.handlers} /> + */ +export type MouseHook = { + readonly is: { readonly over: boolean; readonly down: boolean; readonly dragging: boolean }; + readonly handlers: { onMouseDown: M; onMouseUp: M; onMouseEnter: M; onMouseLeave: M }; + readonly drag?: t.MouseMovement; + reset(): void; +}; + +/** + * Properties passed to the `useMouse` hook. + */ +export type MouseHookProps = { + onDown?: M; + onUp?: M; + onEnter?: M; + onLeave?: M; + onDrag?: t.UseMouseDragHandler; +}; diff --git a/code/sys.ui/ui-react/src/m.use/t.use.SizeObserver.ts b/code/sys.ui/ui-react/src/m.use/t.use.SizeObserver.ts new file mode 100644 index 0000000000..23e9a7ee8b --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/t.use.SizeObserver.ts @@ -0,0 +1,60 @@ +import type { RefCallback } from 'react'; +import type { t } from './common.ts'; + +/** + * Hook Factory: monitor size changes to a DOM element using [ResizeObserver]. + */ +export type UseSizeObserver = <T extends HTMLElement>( + onChange?: t.SizeObserverChangeHandler, +) => t.SizeObserverHook<T>; + +/** + * Hook: monitor size changes to a DOM element using [ResizeObserver]. + */ +export type SizeObserverHook<T extends HTMLElement> = { + /** Callback ref to be assigned to the element to observe. */ + readonly ref: RefCallback<T>; + + /** Flag indicating if the first render-pass has allowed a measurement. */ + readonly ready: boolean; + + /** The latest dimensions of the element (or null if not measured yet). */ + readonly rect?: DOMRectReadOnly; + + /** The `rect.width` value. */ + readonly width?: t.Pixels; + + /** The `rect.height` value. */ + readonly height: t.Pixels; + + /** Convert to a simple object. */ + toObject(): t.DomRect; + + /** Create a string representation of the size. */ + toString(): string; + + /** Renders a react element "<width> x <height>". */ + toElement(props?: SizeObserverElementProps | t.CssEdgesArray): t.ReactNode; +}; + +/** + * Fires when the size of an observed DOM element changes. + */ +export type SizeObserverChangeHandler = (e: SizeObserverChangeHandlerArgs) => void; +export type SizeObserverChangeHandlerArgs = { + readonly rect: DOMRectReadOnly; + toObject(): t.DomRect; +}; + +/** + * Properties for the size rendered as an element ("<width> x <height>"). + */ +export type SizeObserverElementProps = { + /** Display: "block" (false, default) or inline-block (true). */ + inline?: boolean; + visible?: boolean; + fontSize?: number; + opacity?: number; + style?: t.CssInput; + Absolute?: t.CssEdgesInput; +}; diff --git a/code/sys.ui/ui-react/src/m.use/useClick.ts b/code/sys.ui/ui-react/src/m.use/use.Click.ts similarity index 63% rename from code/sys.ui/ui-react/src/m.use/useClick.ts rename to code/sys.ui/ui-react/src/m.use/use.Click.ts index 831d61e9ad..724ffff595 100644 --- a/code/sys.ui/ui-react/src/m.use/useClick.ts +++ b/code/sys.ui/ui-react/src/m.use/use.Click.ts @@ -1,44 +1,48 @@ import { useEffect, useRef } from 'react'; -import type { t } from '../common.ts'; +import type { t } from './common.ts'; type E = HTMLElement; type Div = HTMLDivElement; type MouseEventName = 'mousedown' | 'mouseup'; - const DEFAULT_STAGE: t.UseClickStage = 'down'; +/** + * Hook: Monitors for click events outside the given element. + * Usage: + * Useful for clicking away from modal dialogs or popups. + */ export const useClickOutside: t.UseClickHook = <T extends E = Div>(input: t.UseClickInput<T>) => { - const { callback, stage, ref, event } = wrangle.args<T>(input); - - useEffect(() => { - const handler = (e: MouseEvent) => { - if (!ref.current?.contains(e.target as E)) callback?.(e); - }; - document?.addEventListener(event, handler, true); - return () => document?.removeEventListener(event, handler, true); - }, [event, ref, callback]); - - const api: t.UseClick<T> = { ref, stage }; - return api; + return useHandler<T>(input, (el, e) => !el.contains(e.target as E)); }; +/** + * Hook: Monitors for click events within the given element. + * Usage: + * Useful for clicking away from modal dialogs or popups. + */ export const useClickInside: t.UseClickHook = <T extends E = Div>(input: t.UseClickInput<T>) => { - const { callback, stage, ref, event } = wrangle.args<T>(input); + return useHandler<T>(input, (el, e) => el.contains(e.target as E)); +}; +/** + * Helpers + */ +function useHandler<T extends E = Div>( + input: t.UseClickInput<T>, + shouldInvoke: (el: E, e: MouseEvent) => boolean, +): t.ClickHook<T> { + const { callback, stage, ref, event } = wrangle.args<T>(input); useEffect(() => { const handler = (e: MouseEvent) => { - if (ref.current?.contains(e.target as E)) callback?.(e); + const el = ref.current; + if (el && shouldInvoke(el, e)) callback?.(e); }; document?.addEventListener(event, handler, true); return () => document?.removeEventListener(event, handler, true); }, [event, ref, callback]); + return { ref, stage }; +} - return { ref, stage } as const; -}; - -/** - * Helpers - */ const wrangle = { args<T extends E>(input: t.UseClickInput<T>) { const { callback, stage = DEFAULT_STAGE, ref = useRef<T>(null) } = wrangle.input<T>(input); @@ -46,7 +50,7 @@ const wrangle = { return { callback, event, ref, stage }; }, - input<T extends E>(input: t.UseClickInput<T>): t.UseClickProps<T> { + input<T extends E>(input: t.UseClickInput<T>): t.ClickHookProps<T> { if (typeof input === 'object') return input; if (typeof input === 'function') return { callback: input }; throw new Error('Unable to parse parameter input'); diff --git a/code/sys.ui/ui-react/src/m.use/use.Dist.sample.ts b/code/sys.ui/ui-react/src/m.use/use.Dist.sample.ts new file mode 100644 index 0000000000..5333b1ae6f --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/use.Dist.sample.ts @@ -0,0 +1,70 @@ +import { type t } from './common.ts'; + +export const sample: t.DistPkg = { + '-type:': 'jsr:@sys/types:DistPkg', + pkg: { + name: 'sample', + version: '0.0.0-sample.0', + }, + size: { + bytes: 1119584, + }, + entry: 'pkg/-entry.CsmS4pX8.js', + hash: { + digest: `sha256-0000000000000000000000000000000000000000000000000000000000000000`, + parts: { + 'index.html': `sha256-73118ead01ff731394195389afe6b3fef53f366c92013f90d4098523ef783dc1`, + 'pkg/-entry.CsmS4pX8.js': `sha256-d7c1df2d1d7ccadd611582d340245b446930d9c552b9a6e390e573fcb8069281`, + 'pkg/-pkg.json': `sha256-5cb551c509271d47374b378e56b137f2c30b4ad629fa12836be67f2c896e4565`, + 'pkg/a.CzfbkkOd.svg': `sha256-03dc57becc131b98626bf145a3827966fec98f60e6c284bce7d73a0f137904b7`, + 'pkg/a.DytWQd0P.svg': `sha256-548482b24965277eadcafc4cb299c9b37f7a6583f7ec624dff76b7a4501022f1`, + 'pkg/a.ccVBcAen.css': `sha256-1d6d1a552eabe1df220ee93e445c4f5eb150fdfae998996cdf1f9c60d662c35b`, + 'pkg/m.2f5gzOW6.js': `sha256-5f08d0a5ed787b51ff0e3c8a140668b95fe77cfbabb9fa1b8df085029479bcf5`, + 'pkg/m.6enj-n6N.js': `sha256-3c1100938d2bb3674f3cb696df5b5096151115c024752f7401e84af9327c01d1`, + 'pkg/m.6tNfJC1I.js': `sha256-ae42722c296c1f201a48042a6eaeed858de98585c6d7e44114124b63d1dd5708`, + 'pkg/m.B07hVUpE.js': `sha256-ac6c000836d8e949047f0f367987c38ea81ef14e4963d1e722b291b0bd6ebda3`, + 'pkg/m.B0X5Tzpk.js': `sha256-c4131f76f53fdb76802f60bf7679ccd79a0e390c77b15e7f91b0508a99f954cb`, + 'pkg/m.B7Cwm47C.js': `sha256-3a6e56f9f3e1a717e767f39b296065b868498bdbb7dd6e2fbec75c5c1cc75baf`, + 'pkg/m.B9s02YtB.js': `sha256-5dbfbcbdfd1f50af03605883b36550a847179f6a9f778024ab614537c610a545`, + 'pkg/m.BP6klHZP.js': `sha256-8a7cda4c9f20ade3b84b838c1c1e67cf92a4a93eaebf5744e0fc91b7a0f01608`, + 'pkg/m.BVKZFYvx.js': `sha256-e179d583ae484267be2adda81a83ae0b5c4c521013d4ba4d5291eed7a46414f8`, + 'pkg/m.Ba70fsEO.js': `sha256-af588a67a5779dd492ed3fc6726d6a35d0003bbabba92e45959303912823a899`, + 'pkg/m.Bb98Nh4K.js': `sha256-06e575191386b05b18d7b56f5d5815a0d32fde5908e6043fb50e988397f9516f`, + 'pkg/m.BbGMuf27.js': `sha256-15618e12d24c5ba50d643815909a3001d531a0934ad5375bf5bd425992722742`, + 'pkg/m.BdteEkmZ.js': `sha256-cbd24c58fa9a3608d58919c463d4b83452452b7f998ccbdfefa0a4d629bb92c2`, + 'pkg/m.BsznwMR0.js': `sha256-0e029fcf1770c1e3eca2190a51629683765a744a4c843bdf9973451eae8a0158`, + 'pkg/m.By5VB92O.js': `sha256-02f2948e9425f86b36acc791d162a8fba92e63f71752f1925b467a1df72e470d`, + 'pkg/m.C2DGtZV2.js': `sha256-b50212068d7556c7c3a270826987451a40b32580cbd8dd005bac94b9a0f109c3`, + 'pkg/m.CDGHaSNw.js': `sha256-bf76f8f2ca47251fda3ce17238c6dd3267b115ea20c92b5883a13477ca3045fa`, + 'pkg/m.CFTGxaG4.js': `sha256-66fb099e30d23f8b59f85e6c37528146c9feb73cb5b3961c83f24e6a590858b1`, + 'pkg/m.CHDC_kiv.js': `sha256-ca030dabf34384d92bc2eb4cc750ecc500e175f4835012425616bdef583e7aa7`, + 'pkg/m.CKXg8R_g.js': `sha256-6a2069d6fb2a8cac27f1295d63214574d72950e0bda2da6cf49db6464cae43dd`, + 'pkg/m.CbXFlvb7.js': `sha256-ce20c4bd834cb948ba8868a404f315fecc4870c3672ee4bffd6f924e28f8cca3`, + 'pkg/m.CfKhKnmU.js': `sha256-f704ea57f6887e969b23dd68a81e6b1b2cf87180cb10d1cd731b9e58863135a6`, + 'pkg/m.Ct43jJ6a.js': `sha256-25e470e34075596c9b16366110b3d5bb7b1fc53df5c840cc2d79bb1fcd77de28`, + 'pkg/m.D0Y5-x_C.js': `sha256-05b222be0f2534df761e1debafeae232c01a4d751052a322ca5a685797a54773`, + 'pkg/m.DHs9BToK.js': `sha256-74f9c41114fafa8802c9ce463701a81b9d20d6946c264fa9926e14ca79481ae8`, + 'pkg/m.DKz1sTmA.js': `sha256-c03856d8265824d1aa72ce4e79e43fe03a65ff006b4d9abd41844b3d324a7488`, + 'pkg/m.DM43RIpC.js': `sha256-aacf9d80efe2de10ca655096ea827ab800bc3cbac057812f3cd9e6cf569767f1`, + 'pkg/m.DQgAKyyW.js': `sha256-10716acd89135dc8a996248c715984ae43dbb27d4ddb46177e1e87262d997c2f`, + 'pkg/m.DQt_l2DB.js': `sha256-d21dfba34b433bc9094a08781f42e3def01a757f676b6c61d9883a2f4b14a549`, + 'pkg/m.DXE146RK.js': `sha256-6587c734ae87115dc4bf32f6bf69138b4cba932216f5ee77000ea4f23139a3d7`, + 'pkg/m.DXPUWGVB.js': `sha256-3871a837d066052dc182e679a4d2be70811288c4339d2d777388072d2a7e8cea`, + 'pkg/m.D_9-kWoO.js': `sha256-dd7e221ca2fc58d59037a993b665812b4f0bb66b2254809faceb2a8c5f1584d7`, + 'pkg/m.Ddt9-e-a.js': `sha256-e827694e986a42511675c90a792219523dfb99032487bbd5b637cd507ff784cb`, + 'pkg/m.Djn_Vfvz.js': `sha256-70e2ee39f26a86fac3ed0b5bf74b8bbc4de933742d1c73ede33de10032848610`, + 'pkg/m.DrA-il_j.js': `sha256-0037ace80faf4f1a853aaf9fdcbb4705d15d8abeca787084d52974917e84867d`, + 'pkg/m.DrIctJra.js': `sha256-7b836d03e688109126ffd162b6c559a42d26bf448de166ae7b165a8199b33be4`, + 'pkg/m.DsmzfuLH.js': `sha256-9d4d8e9bdb88c1e83aaaf11471de9017a7cc7df854f40541825e8dab4aeeea00`, + 'pkg/m.DtyQKGjb.js': `sha256-371e390c42e41ae478585eb8304d9cb27407a584921811d7980c3e1e9a83dc29`, + 'pkg/m.DzC4PHOS.js': `sha256-04ae4f600c2b51bb4201b2112afbc376f7791d8b44745ab8e67e5e73ae4a93eb`, + 'pkg/m.FxkjFhCh.js': `sha256-c724be1906d07ecc719c89c5043408f4ad4e5dfa516010d0d50e41758ce0fb35`, + 'pkg/m.JsoYGwwg.js': `sha256-5bdaf5acfd33691ef753bac1732c5564ba04bde4679d259f7a43dad12d7b0429`, + 'pkg/m.Vh11PLjH.js': `sha256-9cd18978f95f944ead11d863a90b483e1658a562c397b7251bb882451061f1d1`, + 'pkg/m.hGJfSRVd.js': `sha256-6aa41025318cfb8f947a12d09952ce080e6e378a4ca4ac0f345b5e02555cc9e6`, + 'pkg/m.ha9d0YpI.js': `sha256-01d0fc7de0feb7b1e291605122020cc18e613455dee99170bd4840d49e71ea19`, + 'pkg/m.kNRDy6Ji.js': `sha256-957cb7554527f2aa004eb576e7738903a4869a00ae3388b26fb05aa61f203607`, + 'pkg/m.txGTKLjD.js': `sha256-00306b58d1f2a9f767df5ee8f36ee497a5858b8209dd52cef7162314b2b55d59`, + }, + }, +}; diff --git a/code/sys.ui/ui-react/src/m.use/use.Dist.ts b/code/sys.ui/ui-react/src/m.use/use.Dist.ts new file mode 100644 index 0000000000..4a92b31004 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/use.Dist.ts @@ -0,0 +1,65 @@ +import { Http } from '@sys/http'; +import { useEffect, useRef, useState } from 'react'; +import { type t, Err } from './common.ts'; + +/** + * Hook: Load the `dist.json` file from the server (if avilable). + */ +export const useDist: t.UseDistFactory = (options = {}) => { + const { useSampleFallback = false } = options; + const is: t.UseDist['is'] = { sample: useSampleFallback }; + + const [count, setRender] = useState(0); + const redraw = () => setRender((n) => n + 1); + + const jsonRef = useRef<t.DistPkg>(); + const [error, setError] = useState<t.StdError>(); + + /** + * Effect: Fetch JSON (or optionally load sample data). + */ + useEffect(() => { + const fetch = Http.fetch(); + + const update = (dist?: t.DistPkg) => { + jsonRef.current = dist; + redraw(); + }; + + const loadJson = async () => { + jsonRef.current = undefined; + const res = await fetch.json<t.DistPkg>('./dist.json'); + if (fetch.disposed) return; + if (res.ok) update(res.data); + else { + setError(Err.std(res.error)); + } + }; + + const loadSample = async () => { + jsonRef.current = undefined; + const m = await import('./use.Dist.sample.ts'); + update(m?.sample); + }; + + const finish = async () => { + if (!jsonRef.current && is.sample) loadSample(); + }; + + loadJson().then(finish).catch(finish); + return fetch.dispose; + }, [is.sample]); + + /** + * API + */ + const api: t.UseDist = { + count, + is, + error, + get json() { + return jsonRef.current; + }, + }; + return api; +}; diff --git a/code/sys.ui/ui-react/src/m.use/use.Is.TouchSupported.ts b/code/sys.ui/ui-react/src/m.use/use.Is.TouchSupported.ts new file mode 100644 index 0000000000..65f5363901 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/use.Is.TouchSupported.ts @@ -0,0 +1,8 @@ +import type { t } from './common.ts'; + +/** + * Hook: detect if the device supports touch events. + */ +export const useIsTouchSupported: t.UseIsTouchSupported = () => { + return typeof window !== 'undefined' && 'ontouchstart' in window; +}; diff --git a/code/sys.ui/ui-react/src/m.use/useMouse.Drag.ts b/code/sys.ui/ui-react/src/m.use/use.Mouse.Drag.ts similarity index 93% rename from code/sys.ui/ui-react/src/m.use/useMouse.Drag.ts rename to code/sys.ui/ui-react/src/m.use/use.Mouse.Drag.ts index d3c18f8494..91719deb94 100644 --- a/code/sys.ui/ui-react/src/m.use/useMouse.Drag.ts +++ b/code/sys.ui/ui-react/src/m.use/use.Mouse.Drag.ts @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; -import type { t } from '../common.ts'; +import type { t } from './common.ts'; /** * Internal hook that trackes mouse movement events (drag). */ -export const useMouseDrag: t.UseMouseDragHook = (props = {}) => { +export const useMouseDrag: t.UseMouseDrag = (props = {}) => { const enabled = Boolean(props.onDrag); const [dragging, setDragging] = useState(false); const [movement, setMovement] = useState<t.MouseMovement>(); @@ -47,7 +47,7 @@ export const useMouseDrag: t.UseMouseDragHook = (props = {}) => { /** * API */ - const api: t.UseMouseDrag = { + const api: t.MouseDragHook = { is: { dragging }, enabled, movement, diff --git a/code/sys.ui/ui-react/src/m.use/useMouse.ts b/code/sys.ui/ui-react/src/m.use/use.Mouse.ts similarity index 87% rename from code/sys.ui/ui-react/src/m.use/useMouse.ts rename to code/sys.ui/ui-react/src/m.use/use.Mouse.ts index b6808fe61e..431fdab3ce 100644 --- a/code/sys.ui/ui-react/src/m.use/useMouse.ts +++ b/code/sys.ui/ui-react/src/m.use/use.Mouse.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; -import type { t } from '../common.ts'; -import { useMouseDrag } from './useMouse.Drag.ts'; +import type { t } from './common.ts'; +import { useMouseDrag } from './use.Mouse.Drag.ts'; /** * Hook: keep track of mouse events for an HTML element @@ -8,9 +8,8 @@ import { useMouseDrag } from './useMouse.Drag.ts'; * * const mouse = useMouse(); * <div {...mouse.handlers} /> - * */ -export const useMouse: t.UseMouseHook = (props = {}) => { +export const useMouse: t.UseMouse = (props = {}) => { const { onDrag } = props; const [isDown, setDown] = useState(false); const [isOver, setOver] = useState(false); @@ -38,7 +37,7 @@ export const useMouse: t.UseMouseHook = (props = {}) => { /** * API */ - const api: t.UseMouse = { + const api: t.MouseHook = { is: { over: isOver, down: isDown, dragging: drag.is.dragging }, handlers: { onMouseDown, onMouseUp, onMouseEnter, onMouseLeave }, drag: drag.movement, diff --git a/code/sys.ui/ui-react/src/m.use/use.SizeObserver.tsx b/code/sys.ui/ui-react/src/m.use/use.SizeObserver.tsx new file mode 100644 index 0000000000..ca541f15a5 --- /dev/null +++ b/code/sys.ui/ui-react/src/m.use/use.SizeObserver.tsx @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useState } from 'react'; +import { type t, css } from './common.ts'; + +/** + * Hook Factory: monitor size changes to a DOM element using [ResizeObserver]. + */ +export const useSizeObserver: t.UseSizeObserver = <T extends HTMLElement>( + onChange?: t.SizeObserverChangeHandler, +) => { + const ref = useCallback((el: T | null) => setElement(el), []); + + const [element, setElement] = useState<T | null>(null); + const [rect, setRect] = useState<DOMRectReadOnly | undefined>(); + + /** + * Effect: monitor DOM element size. + */ + useEffect(() => { + if (!element) return; + if (typeof ResizeObserver !== 'function') return; + + const observer = new ResizeObserver((entries) => { + entries + .filter((entry) => entry.target === element) + .forEach((entry) => setRect(entry.contentRect)); + }); + + observer.observe(element); + return () => observer.disconnect(); + }, [element]); + + /** + * Effect: alert listeners on change. + */ + useEffect(() => { + if (rect) onChange?.({ rect, toObject }); + }, [rect]); + + /** + * API + */ + const toObject = () => wrangle.asObject(rect); + const api: t.SizeObserverHook<T> = { + ref, + get ready() { + return rect !== undefined; + }, + get width() { + return rect?.width ?? 0; + }, + get height() { + return rect?.height ?? 0; + }, + rect, + toObject, + toString() { + const width = wrangle.sizeString(rect?.width); + const height = wrangle.sizeString(rect?.height); + return `${width} x ${height}`; + }, + toElement(input) { + const props = wrangle.elementProps(input); + const { opacity = 0.3, fontSize = 12, Absolute } = props; + let display = props.visible === false ? 'none' : props.inline ? 'inline-block' : 'block'; + const base = css({ Absolute, display, fontSize, opacity }); + return <div className={css(base, props.style).class}>{`${api.toString()}`}</div>; + }, + }; + return api; +}; + +/** + * Helpers + */ +const wrangle = { + asObject(rect?: DOMRect): t.DomRect { + if (!rect) return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 }; + const { x, y, width, height, top, right, bottom, left } = rect; + return { x, y, width, height, top, right, bottom, left }; + }, + + sizeString(input?: number) { + if (input === undefined) return '-'; + return input.toFixed(0); + }, + + elementProps(input?: t.SizeObserverElementProps | t.CssEdgesArray): t.SizeObserverElementProps { + if (!input) return {}; + if (Array.isArray(input)) return { Absolute: input }; + return input; + }, +} as const; diff --git a/code/sys.ui/ui-react/src/mod.ts b/code/sys.ui/ui-react/src/mod.ts index fd0a07a0a1..2bce50147e 100644 --- a/code/sys.ui/ui-react/src/mod.ts +++ b/code/sys.ui/ui-react/src/mod.ts @@ -31,6 +31,19 @@ export { pkg } from './pkg.ts'; export type * as t from './types.ts'; +/** + * Library + */ export { FC } from './m.FC/mod.ts'; -export { useClickInside, useClickOutside, useMouse, useMouseDrag } from './m.use/mod.ts'; -export { ReactEvent } from './u/mod.ts'; +export { Signal } from './m.Signal/mod.ts'; +export { ReactEvent, ReactString } from './u/mod.ts'; + +export { + useClickInside, + useClickOutside, + useDist, + useIsTouchSupported, + useMouse, + useMouseDrag, + useSizeObserver, +} from './m.use/mod.ts'; diff --git a/code/sys.ui/ui-react/src/pkg.ts b/code/sys.ui/ui-react/src/pkg.ts index 79cb3f9c80..054c375c03 100644 --- a/code/sys.ui/ui-react/src/pkg.ts +++ b/code/sys.ui/ui-react/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/ui-react', version: '0.0.94' }; diff --git a/code/sys.ui/ui-react/src/types.ts b/code/sys.ui/ui-react/src/types.ts index 2e49dadcb4..69214ae96b 100644 --- a/code/sys.ui/ui-react/src/types.ts +++ b/code/sys.ui/ui-react/src/types.ts @@ -6,6 +6,8 @@ import type { JSX } from 'react'; export type { FC } from 'react'; export type * from './m.FC/t.ts'; +export type * from './m.Signal/t.ts'; +export type * from './m.Testing.Server/t.ts'; export type * from './m.use/t.ts'; export type * from './u/t.ts'; diff --git a/code/sys.ui/ui-react/src/u/m.ReactString.tsx b/code/sys.ui/ui-react/src/u/m.ReactString.tsx new file mode 100644 index 0000000000..99508d6a1f --- /dev/null +++ b/code/sys.ui/ui-react/src/u/m.ReactString.tsx @@ -0,0 +1,20 @@ +import { Fragment } from 'react'; +import { type t } from './common.ts'; + +export const ReactString: t.ReactStringLib = { + break(input: string | t.ReactNode) { + if (typeof input !== 'string') return input; + return input + .trim() + .split('\n') + .map((line, index, array) => { + const isLast = index === array.length - 1; + return ( + <Fragment key={index}> + <span>{line.trim()}</span> + {!isLast && <br />} + </Fragment> + ); + }); + }, +}; diff --git a/code/sys.ui/ui-react/src/u/mod.ts b/code/sys.ui/ui-react/src/u/mod.ts index a779f58aae..971265cb37 100644 --- a/code/sys.ui/ui-react/src/u/mod.ts +++ b/code/sys.ui/ui-react/src/u/mod.ts @@ -1 +1,2 @@ export { ReactEvent } from './m.ReactEvent.ts'; +export { ReactString } from './m.ReactString.tsx'; diff --git a/code/sys.ui/ui-react/src/u/t.ts b/code/sys.ui/ui-react/src/u/t.ts index 9ed65279b6..e45b4f2b73 100644 --- a/code/sys.ui/ui-react/src/u/t.ts +++ b/code/sys.ui/ui-react/src/u/t.ts @@ -8,3 +8,14 @@ export type ReactEventLib = { /** Convert react mouse events into keyboard modifier info object. */ modifiers(e: MouseEvent): t.KeyboardModifierFlags; }; + +/** + * Helpers for working with strings in react. + */ +export type ReactStringLib = { + /** + * Breaks a string with newline ("\n") characters into a fragment + * of <span>'s and <br> elements. + */ + break(text: string | t.ReactNode): t.ReactNode; +}; diff --git a/code/sys/cli/deno.json b/code/sys/cli/deno.json index d5737f30f4..2306c3a81a 100644 --- a/code/sys/cli/deno.json +++ b/code/sys/cli/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/cli", - "version": "0.0.64", + "version": "0.0.74", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/cli/src/pkg.ts b/code/sys/cli/src/pkg.ts index 79cb3f9c80..75dbdc51c3 100644 --- a/code/sys/cli/src/pkg.ts +++ b/code/sys/cli/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/cli', version: '0.0.74' }; diff --git a/code/sys/cmd/deno.json b/code/sys/cmd/deno.json index 13a25a19ca..94c062b9b9 100644 --- a/code/sys/cmd/deno.json +++ b/code/sys/cmd/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/cmd", - "version": "0.0.80", + "version": "0.0.90", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys/cmd/src/common/libs.ts b/code/sys/cmd/src/common/libs.ts index 54b38639df..47ea2f7bd0 100644 --- a/code/sys/cmd/src/common/libs.ts +++ b/code/sys/cmd/src/common/libs.ts @@ -1 +1 @@ -export { Immutable, ObjectPath, R, Time, Value, rx, slug, isObject } from '@sys/std'; +export { Dispose, Immutable, isObject, ObjectPath, R, rx, slug, Time, Value } from '@sys/std'; diff --git a/code/sys/cmd/src/m.Cmd/Cmd.ts b/code/sys/cmd/src/m.Cmd/Cmd.ts deleted file mode 100644 index a2d22310ae..0000000000 --- a/code/sys/cmd/src/m.Cmd/Cmd.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { create } from './Cmd.impl.ts'; -import { Events, Is, Patch, Path, Queue, toPaths, toTransport, toIssuer } from './u.ts'; -import type { t } from './common.ts'; - -export type CmdLib = { - readonly Is: t.CmdIsLib; -}; - -/** - * Command event structure on an observable/syncing CRDT. - * Primitive for building up an actor model ("message passing computer"). - */ -export const Cmd = { - Is, - Path, - Patch, - Events, - Queue, - - /** - * Factory. - */ - create, - - /** - * Helpers. - */ - autopurge: Queue.autopurge, - toTransport, - toPaths, - toIssuer, -} as const; diff --git a/code/sys/cmd/src/m.Cmd/u.Events.ts b/code/sys/cmd/src/m.Cmd/u.Events.ts index 7fa502e95a..59e3d9ba8f 100644 --- a/code/sys/cmd/src/m.Cmd/u.Events.ts +++ b/code/sys/cmd/src/m.Cmd/u.Events.ts @@ -1,4 +1,4 @@ -import { R, rx, type t, type u } from './common.ts'; +import { type t, type u, Dispose, R, rx } from './common.ts'; import { Patch } from './u.Patch.ts'; import { Path } from './u.Path.ts'; @@ -73,9 +73,9 @@ export const Events: t.CmdEventsLib = { } /** - * API + * API: */ - const api: t.CmdEvents<C> = { + const api = rx.toLifecycle<t.CmdEvents<C>>(life, { $, tx$, error$, @@ -92,17 +92,10 @@ export const Events: t.CmdEventsLib = { type R = ReturnType<t.CmdEvents<C>['issuer']>; const issuer = [...issuers, ...wrangle.issuers(input)]; const res = Events.create(doc, { ...options, issuer, dispose$ }); - delete (res as any).dispose; - return res as unknown as R; + return Dispose.omitDispose(res) as R; }, + }); - // Lifecycle. - dispose, - dispose$, - get disposed() { - return life.disposed; - }, - }; return api; }, diff --git a/code/sys/cmd/src/m.Cmd/u.Listener.ts b/code/sys/cmd/src/m.Cmd/u.Listener.ts index 889a6bfa36..3e79377fa5 100644 --- a/code/sys/cmd/src/m.Cmd/u.Listener.ts +++ b/code/sys/cmd/src/m.Cmd/u.Listener.ts @@ -91,7 +91,7 @@ function create<Req extends t.CmdType, Res extends t.CmdType>( /** * API */ - const api: L = { + const api: L = rx.toLifecycle<L>(life, { $, tx, issuer, @@ -136,14 +136,7 @@ function create<Req extends t.CmdType, Res extends t.CmdType>( handlers.timeout.add(fn); return api; }, - - // Lifecycle. - dispose, - dispose$, - get disposed() { - return life.disposed; - }, - }; + }); return api; } diff --git a/code/sys/cmd/src/m.Cmd/u.Queue.ts b/code/sys/cmd/src/m.Cmd/u.Queue.ts index 432238da6c..3d9ee860ed 100644 --- a/code/sys/cmd/src/m.Cmd/u.Queue.ts +++ b/code/sys/cmd/src/m.Cmd/u.Queue.ts @@ -68,19 +68,12 @@ export const Queue: t.CmdQueueLib = { /** * API */ - const api: t.CmdQueueMonitor = { + const api = rx.toLifecycle<t.CmdQueueMonitor>(events, { bounds: { min, max }, get total() { return Queue.totals(cmd); }, - - // Lifecycle. - dispose, - dispose$, - get disposed() { - return events.disposed; - }, - }; + }); return api; }, } as const; diff --git a/code/sys/cmd/src/pkg.ts b/code/sys/cmd/src/pkg.ts index 79cb3f9c80..57e18f061d 100644 --- a/code/sys/cmd/src/pkg.ts +++ b/code/sys/cmd/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/cmd', version: '0.0.90' }; diff --git a/code/sys/color/deno.json b/code/sys/color/deno.json index d29872c11c..093b6cc6d6 100644 --- a/code/sys/color/deno.json +++ b/code/sys/color/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/color", - "version": "0.0.35", + "version": "0.0.45", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/color/src/m.Rgb/-.test.ts b/code/sys/color/src/m.Rgb/-.test.ts index 8c3a5dfd3d..c68c37f863 100644 --- a/code/sys/color/src/m.Rgb/-.test.ts +++ b/code/sys/color/src/m.Rgb/-.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it } from '../-test.ts'; -import { Color } from './m.Color.ts'; +import { Color, Theme } from './mod.ts'; describe('Color', () => { + it('API', () => { + expect(Color.Theme).to.equal(Theme); + }); + describe('Color.format', () => { const test = (value: string | number | boolean | undefined, output?: string) => { expect(Color.format(value)).to.eql(output); @@ -65,11 +69,24 @@ describe('Color', () => { }); describe('Color.theme', () => { + it('create from root API', () => { + const a = Color.Theme.create(); + const b = Color.theme(); + expect(a.name).to.eql(b.name); + }); + + it('toString', () => { + const a = Color.theme(); + const b = Color.theme('Dark'); + expect(a.toString()).to.eql('Light'); + expect(b.toString()).to.eql('Dark'); + }); + it('name: Light (default)', () => { const res1 = Color.theme(); const res2 = Color.theme('Light'); const res3 = Color.theme('Light', 'red', 'salmon'); - expect(res1.name).to.eql('Light'); + expect(res1.toString()).to.eql(res1.name); expect(res1.is.light).to.eql(true); expect(res1.is.dark).to.eql(false); expect(res1.fg).to.eql(Color.DARK); @@ -137,6 +154,15 @@ describe('Color', () => { expect(light.invert()).to.not.equal(light); // NB: monad. }); + it('Theme.invert (static method)', () => { + const a = Color.theme(); + expect(a.toString()).to.eql('Light'); + + const b = Theme.invert(a.name); + expect(b).to.eql('Dark'); + expect(Theme.invert()).to.eql('Dark'); + }); + it('invert: custom colors', () => { const theme = Color.theme('Light', 'red', 'salmon'); const inverted = theme.invert(); diff --git a/code/sys/color/src/m.Rgb/m.Color.ts b/code/sys/color/src/m.Rgb/m.Color.ts index 128b2547dc..f8fae685d5 100644 --- a/code/sys/color/src/m.Rgb/m.Color.ts +++ b/code/sys/color/src/m.Rgb/m.Color.ts @@ -1,18 +1,18 @@ export * from './u.const.ts'; export * from './u.format.ts'; -export * from './u.theme.ts'; import type { t } from './common.ts'; import { COLORS } from './u.const.ts'; import { alpha, darken, format, lighten } from './u.format.ts'; -import { theme } from './u.theme.ts'; +import { Theme } from './m.Theme.ts'; /** * Library: Helpers for working with colors. */ export const Color: t.ColorLib = { ...COLORS, - theme, + Theme, + theme: Theme.create, format, alpha, lighten, diff --git a/code/sys/color/src/m.Rgb/u.theme.ts b/code/sys/color/src/m.Rgb/m.Theme.ts similarity index 75% rename from code/sys/color/src/m.Rgb/u.theme.ts rename to code/sys/color/src/m.Rgb/m.Theme.ts index f6753120b9..862f674c2a 100644 --- a/code/sys/color/src/m.Rgb/u.theme.ts +++ b/code/sys/color/src/m.Rgb/m.Theme.ts @@ -9,7 +9,7 @@ const defaultTheme: t.CommonTheme = 'Light'; /** * A color theme helper object. */ -export function theme( +export function create( input?: t.CommonTheme | t.ColorTheme | null, // NB: loose input. defaultLight?: ColorInput, defaultDark?: ColorInput, @@ -28,8 +28,8 @@ function factory( defaultDark?: ColorInput, ): t.ColorTheme { const fg = wrangle.color(name, defaultLight, defaultDark); - const bg = wrangle.color(wrangle.invert(name), defaultLight, defaultDark); - return { + const bg = wrangle.color(invert(name), defaultLight, defaultDark); + const theme: t.ColorTheme = { name, fg, bg, @@ -46,10 +46,27 @@ function factory( }, }; }, - invert: () => theme(wrangle.invert(name), defaultLight, defaultDark), - } as const; + invert: () => create(invert(name), defaultLight, defaultDark), + toString: () => name, + }; + return theme; } +/** + * Invert a color scheme. + */ +export function invert(theme: t.CommonTheme = defaultTheme): t.CommonTheme { + return theme === 'Dark' ? 'Light' : 'Dark'; +} + +/** + * API + */ +export const Theme: t.ColorThemeLib = { + create, + invert, +}; + /** * Helpers */ @@ -59,8 +76,4 @@ const wrangle = { const dark = defaultDark ?? WHITE; return theme === 'Dark' ? dark : light; }, - - invert(theme: t.CommonTheme = defaultTheme): t.CommonTheme { - return theme === 'Dark' ? 'Light' : 'Dark'; - }, } as const; diff --git a/code/sys/color/src/m.Rgb/mod.ts b/code/sys/color/src/m.Rgb/mod.ts index 4b22e3350d..6ff8afd72e 100644 --- a/code/sys/color/src/m.Rgb/mod.ts +++ b/code/sys/color/src/m.Rgb/mod.ts @@ -11,4 +11,5 @@ * const myColor = Color.alpha(theme.fg, 0.3); * ``` */ -export { COLORS, Color } from './m.Color.ts'; +export { Color, COLORS } from './m.Color.ts'; +export { Theme } from './m.Theme.ts'; diff --git a/code/sys/color/src/m.Rgb/t.ts b/code/sys/color/src/m.Rgb/t.ts index 86aaca344d..0a006dd60a 100644 --- a/code/sys/color/src/m.Rgb/t.ts +++ b/code/sys/color/src/m.Rgb/t.ts @@ -30,14 +30,29 @@ export type ColorLib = t.ColorConstants & { */ darken(color: string, amount: number): string; + /** ColorThemeLib */ + readonly Theme: ColorThemeLib; + /** Create a color theme instance. */ + theme: ColorThemeLib['create']; +}; + +/** + * Tools for working with the basic color theme ("Light" / "Dark"). + */ +export type ColorThemeLib = { /** * Create a color theme instance. */ - theme( + create( input?: t.CommonTheme | t.ColorTheme | null, // NB: loose input. defaultLight?: ColorInput, defaultDark?: ColorInput, ): t.ColorTheme; + + /** + * Invert a color scheme. + */ + invert(theme?: t.CommonTheme): t.CommonTheme; }; /** @@ -61,6 +76,9 @@ export type ColorTheme = ColorThemeColors & { /** Retrieve a new theme inverted (eg. "Dark" ā "Light") */ invert(): ColorTheme; + + /** Convert to string. */ + toString(): string; }; /** diff --git a/code/sys/color/src/pkg.ts b/code/sys/color/src/pkg.ts index 79cb3f9c80..cf2b90315f 100644 --- a/code/sys/color/src/pkg.ts +++ b/code/sys/color/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/color', version: '0.0.45' }; diff --git a/code/sys/crypto/deno.json b/code/sys/crypto/deno.json index 78365de310..ee81912b4f 100644 --- a/code/sys/crypto/deno.json +++ b/code/sys/crypto/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/crypto", - "version": "0.0.64", + "version": "0.0.74", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/crypto/src/pkg.ts b/code/sys/crypto/src/pkg.ts index 79cb3f9c80..8cf55a17c5 100644 --- a/code/sys/crypto/src/pkg.ts +++ b/code/sys/crypto/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/crypto', version: '0.0.74' }; diff --git a/code/sys/fs/deno.json b/code/sys/fs/deno.json index 226f44cd2b..df8dd09e46 100644 --- a/code/sys/fs/deno.json +++ b/code/sys/fs/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/fs", - "version": "0.0.79", + "version": "0.0.89", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/fs/src/-test/-sample-files/volcano.jpg b/code/sys/fs/src/-test/-sample-files/volcano.jpg new file mode 100644 index 0000000000..ab0cef116f Binary files /dev/null and b/code/sys/fs/src/-test/-sample-files/volcano.jpg differ diff --git a/code/sys/fs/src/common/libs.ts b/code/sys/fs/src/common/libs.ts index 35ae282c51..93432962c7 100644 --- a/code/sys/fs/src/common/libs.ts +++ b/code/sys/fs/src/common/libs.ts @@ -1,4 +1,4 @@ -export { ensureDir, exists } from '@std/fs'; +export { ensureDir, ensureSymlink, exists } from '@std/fs'; export { Date, Delete, R, rx, slug, Time } from '@sys/std'; export { Err } from '@sys/std/error'; diff --git a/code/sys/fs/src/common/mod.ts b/code/sys/fs/src/common/mod.ts index 2f2b241bea..fa98d7391f 100644 --- a/code/sys/fs/src/common/mod.ts +++ b/code/sys/fs/src/common/mod.ts @@ -2,4 +2,4 @@ export type * as t from './t.ts'; export { pkg } from '../pkg.ts'; export * from './libs.ts'; -export * from './u.ts'; +export * from './u.is.ts'; diff --git a/code/sys/fs/src/common/u.is.ts b/code/sys/fs/src/common/u.is.ts new file mode 100644 index 0000000000..fd645aa5e8 --- /dev/null +++ b/code/sys/fs/src/common/u.is.ts @@ -0,0 +1,74 @@ +import type * as t from './t.ts'; +import { exists, StdPath } from './libs.ts'; + +/** + * Determine if the given path points to a directory. + */ +export async function isDir(path: t.StringPath | URL) { + try { + path = wrangle.path(path); + if (!(await exists(path))) return false; + return (await Deno.stat(path)).isDirectory; + } catch (_error: any) { + return false; + } +} + +/** + * Determine if the given path points to a file (not a directory). + */ +export async function isFile(path: t.StringPath | URL) { + try { + path = wrangle.path(path); + if (!(await exists(path))) return false; + return (await Deno.stat(path)).isFile; + } catch (_error: any) { + return false; + } +} + +/** + * Checks whether a file is binary. + * It reads up to 8KB of the file and looks for <null? bytes + * and a high percentage of non-printable characters. + */ +export async function isBinary(path: t.StringPath | URL): Promise<boolean> { + path = wrangle.path(path); + if (!(await isFile(path))) return false; + + const data = await Deno.readFile(path); + + // Use a chunk size of 8KB (or the entire file if smaller). + const chunkSize = 8192; + const chunk = data.slice(0, Math.min(data.length, chunkSize)); + + // Check for a null byte. + if (chunk.includes(0)) { + return true; + } + + // Count non-printable characters (excluding common whitespace). + let nonPrintableCount = 0; + for (const byte of chunk) { + // Accept common control chars: tab (9), newline (10), and carriage return (13). + if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) { + nonPrintableCount++; + } + } + + // If more than 30% of the bytes are non-printable, assume it's binary. + if (nonPrintableCount / chunk.length > 0.3) { + return true; + } + + return false; +} + +/** + * Helpers + */ +const wrangle = { + path(path: string | URL) { + return typeof path === 'string' ? StdPath.resolve(path) : path; + }, +} as const; diff --git a/code/sys/fs/src/common/u.ts b/code/sys/fs/src/common/u.ts deleted file mode 100644 index 60526a4119..0000000000 --- a/code/sys/fs/src/common/u.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type * as t from './t.ts'; -import { exists } from './libs.ts'; - -/** - * Determine if the given path points to a directory. - */ -export async function isDir(path: t.StringPath | URL) { - try { - if (!(await exists(path))) return false; - return (await Deno.stat(path)).isDirectory; - } catch (_error: any) { - return false; - } -} - -/** - * Determine if the given path points to a file (not a directory). - */ -export async function isFile(path: t.StringPath | URL) { - try { - if (!(await exists(path))) return false; - return (await Deno.stat(path)).isFile; - } catch (_error: any) { - return false; - } -} diff --git a/code/sys/fs/src/m.Fs/-.test.ts b/code/sys/fs/src/m.Fs/-.test.ts index 30ff6efeec..843688d397 100644 --- a/code/sys/fs/src/m.Fs/-.test.ts +++ b/code/sys/fs/src/m.Fs/-.test.ts @@ -1,3 +1,4 @@ +import * as StdFs from '@std/fs'; import { Path as StdPath } from '@sys/std'; import { describe, expect, it } from '../-test.ts'; import { Glob } from '../m.Glob/mod.ts'; @@ -9,6 +10,9 @@ describe('Fs: filesystem', () => { expect(Fs.glob).to.equal(Glob.create); expect(Fs.ls).to.equal(Glob.ls); expect(Fs.trimCwd).to.equal(Path.trimCwd); + + expect(Fs.ensureDir).to.eql(StdFs.ensureDir); + expect(Fs.ensureSymlink).to.eql(StdFs.ensureSymlink); }); describe('Fs.Path', () => { @@ -36,4 +40,15 @@ describe('Fs: filesystem', () => { expect(res3).to.eql(path3); // NB: not-found, no change. }); }); + + describe('Fs.cwd', () => { + it('returns the CWD', () => { + expect(Fs.cwd()).to.eql(Deno.cwd()); + }); + + it('returns the initiating CWD', () => { + const dir = Fs.cwd('init'); + expect(dir).to.eql(Deno.env.get('INIT_CWD')); + }); + }); }); diff --git a/code/sys/fs/src/m.Fs/-fs.meta.is.test.ts b/code/sys/fs/src/m.Fs/-fs.meta.is.test.ts new file mode 100644 index 0000000000..fdb7572ff1 --- /dev/null +++ b/code/sys/fs/src/m.Fs/-fs.meta.is.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from '../-test.ts'; +import { Path } from './common.ts'; +import { Fs } from './mod.ts'; + +describe('Fs.Is (flags)', () => { + const Is = Fs.Is; + + it('has mapped Path methods', () => { + // NB: mapped helpers (convenience). + expect(Is.absolute).to.equal(Fs.Path.Is.absolute); + expect(Is.glob).to.equal(Fs.Path.Is.glob); + }); + + it('Is.dir', async () => { + expect(await Is.dir('.')).to.eql(true); + expect(await Is.dir(Path.resolve('.'))).to.eql(true); + expect(await Is.dir('./deno.json')).to.eql(false); + expect(await Is.dir('./404.json')).to.eql(false); // NB: target does not exist. + }); + + it('Is.file', async () => { + expect(await Is.file('.')).to.eql(false); + expect(await Is.file('./deno.json')).to.eql(true); + expect(await Is.file(Path.resolve('./deno.json'))).to.eql(true); + expect(await Is.file('./404.json')).to.eql(false); // NB: target does not exist. + }); + + it('Is.binary', async () => { + expect(await Is.binary('.')).to.eql(false); + expect(await Is.binary('./deno.json')).to.eql(false); + expect(await Is.binary('./src/-test/-sample-files/volcano.jpg')).to.eql(true); + }); +}); diff --git a/code/sys/fs/src/m.Fs/-fs.meta.test.ts b/code/sys/fs/src/m.Fs/-fs.meta.test.ts index 3cc2b4e4ce..ae1eed083d 100644 --- a/code/sys/fs/src/m.Fs/-fs.meta.test.ts +++ b/code/sys/fs/src/m.Fs/-fs.meta.test.ts @@ -1,27 +1,21 @@ import { describe, expect, it } from '../-test.ts'; -import { Path } from './common.ts'; import { Fs } from './mod.ts'; describe('Fs: info/meta-data operations on the file-system', () => { - describe('Fs.Is (flags)', () => { - const Is = Fs.Is; - - it('has mapped Path methods', () => { - // NB: mapped helpers (convenience). - expect(Is.absolute).to.equal(Fs.Path.Is.absolute); - expect(Is.glob).to.equal(Fs.Path.Is.glob); - }); - - it('Is.dir', async () => { - expect(await Is.dir(Path.resolve('.'))).to.eql(true); - expect(await Is.dir(Path.resolve('./deno.json'))).to.eql(false); - expect(await Is.dir(Path.resolve('./404.json'))).to.eql(false); // NB: target does not exist. + describe('Fs.stat', () => { + it('file does not exist ā <undefined>', async () => { + const path = Fs.resolve('./404.json'); + const res = await Fs.stat(path); + expect(res).to.eql(undefined); }); - it('Is.file', async () => { - expect(await Is.file(Path.resolve('.'))).to.eql(false); - expect(await Is.file(Path.resolve('./deno.json'))).to.eql(true); - expect(await Is.file(Path.resolve('./404.json'))).to.eql(false); // NB: target does not exist. + it('file exists', async () => { + const path = './src/-test/-sample-1/foo.txt'; + const a = await Fs.stat(Fs.resolve(path)); + const b = await Fs.stat(path); + expect(a?.isFile).to.eql(true); + expect(a?.size).to.be.greaterThan(10); + expect(a).to.eql(b); // NB: auto-resolves path internally. }); }); diff --git a/code/sys/fs/src/m.Fs/m.Fs.ts b/code/sys/fs/src/m.Fs/m.Fs.ts index 34ddc3ae72..451a2fb0ca 100644 --- a/code/sys/fs/src/m.Fs/m.Fs.ts +++ b/code/sys/fs/src/m.Fs/m.Fs.ts @@ -1,4 +1,4 @@ -import { type t, ensureDir, exists, ls, Path } from './common.ts'; +import { type t, ensureDir, ensureSymlink, exists, ls, Path } from './common.ts'; import { create as glob } from '../m.Glob/u.create.ts'; import { Watch } from '../m.Watch/mod.ts'; @@ -7,10 +7,12 @@ import { Size } from './m.Size.ts'; import { copy, copyDir, copyFile } from './u.copy.ts'; import { read, readJson, readText, readYaml } from './u.read.ts'; import { remove } from './u.remove.ts'; +import { stat } from './u.stat.ts'; import { toDir } from './u.toDir.ts'; import { toFile } from './u.toFile.ts'; import { walk, walkUp } from './u.walk.ts'; import { write, writeJson } from './u.write.ts'; +import { cwd } from './u.cwd.ts'; export { Path }; const { join, resolve, basename, dirname, extname } = Path; @@ -23,8 +25,9 @@ export const Fs: t.FsLib = { Path, Size, Watch, - stat: Deno.stat, - cwd: Deno.cwd, + stat, + + cwd, trimCwd: Path.trimCwd, join, @@ -41,6 +44,7 @@ export const Fs: t.FsLib = { exists, ensureDir, + ensureSymlink, remove, read, diff --git a/code/sys/fs/src/m.Fs/m.Is.ts b/code/sys/fs/src/m.Fs/m.Is.ts index 557d39e374..1c5522dae1 100644 --- a/code/sys/fs/src/m.Fs/m.Is.ts +++ b/code/sys/fs/src/m.Fs/m.Is.ts @@ -1,4 +1,4 @@ -import { type t, isDir, isFile, StdPath } from './common.ts'; +import { type t, isBinary, isDir, isFile, StdPath } from './common.ts'; /** * Filesystem/Path type verification flags. @@ -7,4 +7,5 @@ export const Is: t.FsIsLib = { ...StdPath.Is, dir: isDir, file: isFile, + binary: isBinary, } as const; diff --git a/code/sys/fs/src/m.Fs/t.ts b/code/sys/fs/src/m.Fs/t.ts index dc0db550f5..c2f2697915 100644 --- a/code/sys/fs/src/m.Fs/t.ts +++ b/code/sys/fs/src/m.Fs/t.ts @@ -50,11 +50,11 @@ export type FsLib = Methods & { /** Recursively walk up a directory tree (visitor pattern). */ readonly walkUp: t.FsWalkUp; - /** Start a file-system watcher */ + /** Start a file-system watcher. */ readonly watch: t.FsWatchLib['start']; /** Current working directory. */ - cwd(): t.StringDir; + cwd(kind?: 'init'): t.StringDir; /** Removes the CWD (current-working-directory) from the given path if it exists. */ trimCwd: t.FsPathLib['trimCwd']; @@ -116,6 +116,9 @@ type StdMethods = { /** Asynchronously ensures that the directory exists, like `mkdir -p.` */ readonly ensureDir: typeof StdFs.ensureDir; + /** Asynchronously ensures that the link exists, and points to a valid file. */ + readonly ensureSymlink: typeof StdFs.ensureSymlink; + /** Recursively walks through a directory and yields information about each file and directory encountered. */ readonly walk: typeof StdFs.walk; }; @@ -129,12 +132,16 @@ export type FsIsLib = t.PathLib['Is'] & { /** Determine if the given path points to a file (not a directory). */ file(path: t.StringPath | URL): Promise<boolean>; + + /** Determine if the given path points to a binary (non-string) file. */ + binary(path: t.StringPath | URL): Promise<boolean>; }; /** * Retrieve information about the given path. */ -export type FsGetStat = (path: t.StringPath | URL) => Promise<Deno.FileInfo>; +export type FsGetStat = (path: t.StringPath | URL) => Promise<FsFileInfo | undefined>; +export type FsFileInfo = Deno.FileInfo; /** * Copy a file or directory. diff --git a/code/sys/fs/src/m.Fs/u.cwd.ts b/code/sys/fs/src/m.Fs/u.cwd.ts new file mode 100644 index 0000000000..3f2d4f5859 --- /dev/null +++ b/code/sys/fs/src/m.Fs/u.cwd.ts @@ -0,0 +1,10 @@ +import { type t, exists, Path, Err } from './common.ts'; + +/** + * Current Working Directory: + * https://docs.deno.com/runtime/reference/cli/task/#specifying-the-current-working-directory + */ +export const cwd: t.FsLib['cwd'] = (kind) => { + if (kind === 'init') return Deno.env.get('INIT_CWD') ?? Deno.cwd(); + return Deno.cwd(); +}; diff --git a/code/sys/fs/src/m.Fs/u.stat.ts b/code/sys/fs/src/m.Fs/u.stat.ts new file mode 100644 index 0000000000..c138209074 --- /dev/null +++ b/code/sys/fs/src/m.Fs/u.stat.ts @@ -0,0 +1,11 @@ +import { type t, Path, exists } from './common.ts'; + +/** + * Resolves to a Deno.FileInfo for the specified path. + * Will always follow symlinks. + */ +export const stat: t.FsLib['stat'] = async (path: t.StringPath | URL) => { + if (!(await exists(path))) return undefined; + path = typeof path === 'string' ? Path.resolve(path) : path; + return Deno.stat(path); +}; diff --git a/code/sys/fs/src/m.Fs/u.walk.ts b/code/sys/fs/src/m.Fs/u.walk.ts index 9dd4817f66..b832d25de5 100644 --- a/code/sys/fs/src/m.Fs/u.walk.ts +++ b/code/sys/fs/src/m.Fs/u.walk.ts @@ -1,6 +1,5 @@ import { walk, type WalkEntry } from '@std/fs'; - -import { type t, Path } from './common.ts'; +import { Path, type t } from './common.ts'; export { walk }; diff --git a/code/sys/fs/src/m.Pkg/-Pkg.Dist.test.ts b/code/sys/fs/src/m.Pkg/-Pkg.Dist.test.ts index 07199c41f4..b22a49611c 100644 --- a/code/sys/fs/src/m.Pkg/-Pkg.Dist.test.ts +++ b/code/sys/fs/src/m.Pkg/-Pkg.Dist.test.ts @@ -43,6 +43,7 @@ describe('Pkg.Dist', () => { expect(res.exists).to.eql(true); expect(res.error).to.eql(undefined); + expect(res.dist['-type:'] === 'jsr:@sys/types:DistPkg').to.eql(true); expect(res.dir).to.eql(Fs.resolve(dir)); expect(res.dist.pkg).to.eql(pkg); expect(res.dist.entry).to.eql(Path.normalize(entry)); diff --git a/code/sys/fs/src/m.Pkg/m.Dist.ts b/code/sys/fs/src/m.Pkg/m.Dist.ts index 81be24fdea..f7816cd4e4 100644 --- a/code/sys/fs/src/m.Pkg/m.Dist.ts +++ b/code/sys/fs/src/m.Pkg/m.Dist.ts @@ -35,7 +35,13 @@ export const Dist: t.PkgDistFsLib = { const hash = exists ? await wrangle.hashes(dir) : { digest: '', parts: {} }; const bytes = await wrangle.bytes(dir, Object.keys(hash.parts)); const size: t.DistPkg['size'] = { bytes }; - const dist: t.DistPkg = { pkg, size, entry: wrangle.entry(entry), hash }; + const dist: t.DistPkg = { + '-type:': 'jsr:@sys/types:DistPkg', + pkg, + size, + entry: wrangle.entry(entry), + hash, + }; /** * Prepare response. @@ -132,7 +138,7 @@ const wrangle = { let count = 0; for (const file of files) { const stat = await Fs.stat(Fs.join(dir, file)); - count += stat.size; + count += stat?.size ?? 0; } return count; }, diff --git a/code/sys/fs/src/m.Watch/m.Watch.ts b/code/sys/fs/src/m.Watch/m.Watch.ts index d96a19d664..89456c80ce 100644 --- a/code/sys/fs/src/m.Watch/m.Watch.ts +++ b/code/sys/fs/src/m.Watch/m.Watch.ts @@ -12,13 +12,12 @@ export const Watch: t.FsWatchLib = { const paths = asArray(pathInput); const errors = Err.errors(); const life = rx.lifecycle(options.dispose$); - const { dispose, dispose$ } = life; const $$ = rx.subject<t.FsWatchEvent>(); - const $ = $$.pipe(rx.takeUntil(dispose$)); + const $ = $$.pipe(rx.takeUntil(life.dispose$)); let _watcher: Deno.FsWatcher | undefined; - dispose$.subscribe(() => _watcher?.close()); + life.dispose$.subscribe(() => _watcher?.close()); const exists = await wrangle.exists(paths); if (!exists.ok) { @@ -46,7 +45,7 @@ export const Watch: t.FsWatchLib = { /** * API */ - const api: t.FsWatcher = { + const api = rx.toLifecycle<t.FsWatcher>(life, { get $() { return $; }, @@ -62,20 +61,7 @@ export const Watch: t.FsWatchLib = { get error() { return errors.toError('Several errors occured while watching file-system paths.'); }, - - /** Lifecycle: disposes of the watches. */ - dispose, - - /** Lifecycle: observable that fires when the watcher disposes. */ - get dispose$() { - return dispose$; - }, - - /** Lifecycle: flag indicating if the watcher has been disposed. */ - get disposed() { - return life.disposed; - }, - }; + }); return api; }, }; diff --git a/code/sys/fs/src/pkg.ts b/code/sys/fs/src/pkg.ts index 79cb3f9c80..1eaf6ee545 100644 --- a/code/sys/fs/src/pkg.ts +++ b/code/sys/fs/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/fs', version: '0.0.89' }; diff --git a/code/sys/http/deno.json b/code/sys/http/deno.json index a961d4ce98..ba8321ad7e 100644 --- a/code/sys/http/deno.json +++ b/code/sys/http/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/http", - "version": "0.0.46", + "version": "0.0.56", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/http/src/ns.client/m.Http.Fetch/u.create.ts b/code/sys/http/src/ns.client/m.Http.Fetch/u.create.ts index 0c1e2843b0..1aa4eb5fbd 100644 --- a/code/sys/http/src/ns.client/m.Http.Fetch/u.create.ts +++ b/code/sys/http/src/ns.client/m.Http.Fetch/u.create.ts @@ -105,13 +105,12 @@ export const create: F = (input: Parameters<F>[0]) => { } as t.FetchResponse<T>; }; - const api: t.HttpFetch = { + const api: t.HttpFetch = rx.toLifecycle<t.HttpFetch>(life, { + header: (name) => (api.headers as any)[name], get headers() { return wrangle.headers(options); }, - header: (name) => (api.headers as any)[name], - async json<T>(input: RequestInput, init: RequestInit = {}, options = {}) { return invokeFetch<T>('application/json', input, init, options, (res) => res.json()); }, @@ -119,18 +118,7 @@ export const create: F = (input: Parameters<F>[0]) => { async text(input: RequestInput, init: RequestInit = {}, options = {}) { return invokeFetch<string>('text/plain', input, init, options, (res) => res.text()); }, - - /** - * Lifecycle. - */ - dispose: life.dispose, - get dispose$() { - return life.dispose$; - }, - get disposed() { - return life.disposed; - }, - }; + }); return api; }; diff --git a/code/sys/http/src/pkg.ts b/code/sys/http/src/pkg.ts index 79cb3f9c80..83eb7de192 100644 --- a/code/sys/http/src/pkg.ts +++ b/code/sys/http/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/http', version: '0.0.56' }; diff --git a/code/sys/jsr/deno.json b/code/sys/jsr/deno.json index 142751e714..5db51b22d8 100644 --- a/code/sys/jsr/deno.json +++ b/code/sys/jsr/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/jsr", - "version": "0.0.45", + "version": "0.0.55", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/jsr/src/pkg.ts b/code/sys/jsr/src/pkg.ts index 79cb3f9c80..3a6869bc00 100644 --- a/code/sys/jsr/src/pkg.ts +++ b/code/sys/jsr/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/jsr', version: '0.0.55' }; diff --git a/code/sys/main/deno.json b/code/sys/main/deno.json index 17b60f0ddb..be85924086 100644 --- a/code/sys/main/deno.json +++ b/code/sys/main/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/main", - "version": "0.0.57", + "version": "0.0.67", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/main/src/pkg.ts b/code/sys/main/src/pkg.ts index 79cb3f9c80..d01b3ac424 100644 --- a/code/sys/main/src/pkg.ts +++ b/code/sys/main/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/main', version: '0.0.67' }; diff --git a/code/sys/process/deno.json b/code/sys/process/deno.json index 09e58b0c21..b17558603d 100644 --- a/code/sys/process/deno.json +++ b/code/sys/process/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/process", - "version": "0.0.65", + "version": "0.0.75", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/process/src/pkg.ts b/code/sys/process/src/pkg.ts index 79cb3f9c80..c5aba01c0a 100644 --- a/code/sys/process/src/pkg.ts +++ b/code/sys/process/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/process', version: '0.0.75' }; diff --git a/code/sys/std/deno.json b/code/sys/std/deno.json index 4ff47723e0..891672992b 100644 --- a/code/sys/std/deno.json +++ b/code/sys/std/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/std", - "version": "0.0.131", + "version": "0.0.141", "license": "MIT", "tasks": { "test": "deno test -RWN", @@ -25,6 +25,7 @@ "./rx": "./src/m.Rx/mod.ts", "./semver": "./src/m.Semver/mod.ts", "./semver/server": "./src/m.Semver.Server/mod.ts", + "./signal": "./src/m.Signal/mod.ts", "./testing": "./src/m.Testing/mod.ts", "./testing/server": "./src/m.Testing.Server/mod.ts", "./value": "./src/m.Value/mod.ts" diff --git a/code/sys/std/src/common/u.ts b/code/sys/std/src/common/u.ts index dbcb3e341d..e9c0073998 100644 --- a/code/sys/std/src/common/u.ts +++ b/code/sys/std/src/common/u.ts @@ -10,13 +10,13 @@ export function isObject(input: any): input is object { /** * Determine if the given input is a simple {key:value} record object. */ -export function isRecord(input: any): input is O { +export function isRecord<T extends O>(input: any): input is T { return isObject(input) && !Array.isArray(input); } /** * Determine if the given object is empty of all fields. */ -export function isEmptyRecord(input: any): input is object { +export function isEmptyRecord<T extends O>(input: any): input is T { return isRecord(input) && Object.keys(input).length === 0; } diff --git a/code/sys/std/src/m.DateTime/-Time.test.ts b/code/sys/std/src/m.DateTime/-Time.test.ts index 0431f3ba81..8dfca34071 100644 --- a/code/sys/std/src/m.DateTime/-Time.test.ts +++ b/code/sys/std/src/m.DateTime/-Time.test.ts @@ -44,7 +44,7 @@ describe('Time', () => { expect(res.timeout).to.eql(0); }); - it('()=> | ā handler only, defaults to 0:msecs', async () => { + it('()=> | ā handler only, defaults to micro-task ("tick")', async () => { let fired = 0; const startedAt = new Date(); const res = Time.delay(() => fired++); @@ -68,7 +68,7 @@ describe('Time', () => { }); describe('Time.wait', () => { - it('Time.wait( n ) ā number/msecs param', async () => { + it('Time.wait( n ) ā number/msecs param (macro-task queue)', async () => { const startedAt = new Date(); expect(calcDiff(startedAt)).to.be.closeTo(0, 10); @@ -80,15 +80,29 @@ describe('Time', () => { expect(res.is).to.eql({ completed: true, cancelled: false, done: true }); }); - it('Time.wait() ā no param', async () => { - const startedAt = new Date(); - expect(calcDiff(startedAt)).to.be.closeTo(0, 10); + it('Time.wait() ā no param (micro-task, aka. "tick")', async () => { + await Testing.retry(3, async () => { + const timer = Time.timer(); + let microtaskResolved = false; + let macrotaskResolved = false; - const res = Time.wait(); - await res; + const stop = setTimeout(() => (macrotaskResolved = true), 0); // ā Schedule a macro-task for comparison. + const res = Time.wait(); // ā the micro-task delay (no param). + await res; - expect(res.timeout).to.eql(0); - expect(calcDiff(startedAt)).to.be.lessThan(10); + const elapsed = timer.elapsed.msec; + microtaskResolved = true; + + expect(res.is.done).to.eql(true); + expect(res.is.completed).to.eql(true); + expect(res.is.cancelled).to.eql(false); + + expect(microtaskResolved).to.be.true; + expect(macrotaskResolved).to.be.false; // Microtasks should run before macrotasks. + expect(elapsed).to.be.closeTo(0, 2); // Allow up to 2ms tolerance. + + clearTimeout(stop); + }); }); it('Time.wait ā cancel', () => { diff --git a/code/sys/std/src/m.DateTime/-Time.until.test.ts b/code/sys/std/src/m.DateTime/-Time.until.test.ts index 9e93d8892f..bda4c09986 100644 --- a/code/sys/std/src/m.DateTime/-Time.until.test.ts +++ b/code/sys/std/src/m.DateTime/-Time.until.test.ts @@ -31,7 +31,6 @@ describe('Time.until', () => { let count = 0; const time = Time.until(dispose$); time.dispose$.subscribe(() => disposeFired++); - expect(time.disposed).to.eql(false); const res = time.delay(10, () => (count += 1)); @@ -43,5 +42,22 @@ describe('Time.until', () => { expect(time.disposed).to.eql(true); expect(disposeFired).to.eql(1); }); + + it('is a disposable', async () => { + let disposeFired = 0; + let count = 0; + const time = Time.until(); + time.dispose$.subscribe(() => disposeFired++); + expect(time.disposed).to.eql(false); + + const res = time.delay(10, () => (count += 1)); + time.dispose(); + + await res; + await Time.wait(20); + expect(count).to.eql(0); + expect(time.disposed).to.eql(true); + expect(disposeFired).to.eql(1); + }); }); }); diff --git a/code/sys/std/src/m.DateTime/-Timer.test.ts b/code/sys/std/src/m.DateTime/-Timer.test.ts index fc7240ed00..27f7b7f0a7 100644 --- a/code/sys/std/src/m.DateTime/-Timer.test.ts +++ b/code/sys/std/src/m.DateTime/-Timer.test.ts @@ -1,15 +1,17 @@ import { add, startOfDay, sub } from 'date-fns'; -import { describe, expect, it } from '../-test.ts'; +import { describe, expect, it, Testing } from '../-test.ts'; import { Time } from './mod.ts'; const FORMAT = 'yyyy-MM-dd hh:mm:ss'; const format = (date: Date) => Time.utc(date).format(FORMAT); describe('timer', () => { - it('starts with current date', () => { - const now = Time.now.format(FORMAT); - const timer = Time.timer(); - expect(format(timer.startedAt)).to.eql(now); + it('starts with current date', async () => { + await Testing.retry(3, async () => { + const now = Time.now.format(FORMAT); + const timer = Time.timer(); + expect(format(timer.startedAt)).to.eql(now); + }); }); it('starts with given date', () => { diff --git a/code/sys/std/src/m.DateTime/-Timestamp.test.ts b/code/sys/std/src/m.DateTime/-Timestamp.test.ts new file mode 100644 index 0000000000..3f0e22fdc9 --- /dev/null +++ b/code/sys/std/src/m.DateTime/-Timestamp.test.ts @@ -0,0 +1,370 @@ +import { type t, describe, expect, it } from '../-test.ts'; +import { Num } from '../m.Value.Num/mod.ts'; +import { Time } from './m.Time.ts'; +import { Timestamp } from './mod.ts'; + +describe('Timestamp', () => { + type T = { image: string }; + type MyTimestamps = t.Timestamps<T>; + + describe('parse: "HH:MM:SS.mmm"', () => { + it('should parse "00:00:00.000" as 0 msecs and 0 secs', () => { + const res = Timestamp.parse('00:00:00.000'); + expect(res.msec).to.eql(0); + expect(res.sec).to.eql(0); + expect(res.hour).to.eql(0); + expect(res.min).to.eql(0); + }); + + it('should parse "01:02:03.004" correctly', () => { + // Expected values: + // hours: 1, minutes: 2, seconds: 3, milliseconds: 4 + const expectedMsecs = 1 * 3600000 + 2 * 60000 + 3 * 1000 + 4; // 3,723,004 msecs + const expectedSecs = expectedMsecs / 1000; // 3,723.004 secs + const expectedMins = expectedMsecs / 60000; // 62.05006666666667 mins + const expectedHours = expectedMsecs / 3600000; // 1.0341677777777778 hours + + const res = Timestamp.parse('01:02:03.004'); + expect(res.msec).to.eql(expectedMsecs); + expect(res.sec).to.eql(Num.round(expectedSecs, 1)); + expect(res.min).to.eql(Num.round(expectedMins, 1)); + expect(res.hour).to.eql(Num.round(expectedHours, 1)); + }); + + it('should parse "12:34:56.789" correctly', () => { + // Expected values: + // hours: 12, minutes: 34, seconds: 56, milliseconds: 789 + const expectedMsecs = 12 * 3600000 + 34 * 60000 + 56 * 1000 + 789; + const expectedSecs = expectedMsecs / 1000; + const expectedMins = expectedMsecs / 60000; + const expectedHours = expectedMsecs / 3600000; + + const res = Timestamp.parse('12:34:56.789'); + expect(res.msec).to.eql(expectedMsecs); + expect(res.sec).to.eql(Num.round(expectedSecs, 1)); + expect(res.min).to.eql(Num.round(expectedMins, 1)); + expect(res.hour).to.eql(Num.round(expectedHours, 1)); + }); + + it('should fix single-digit unit values', () => { + const res = Timestamp.parse('1:2:3.4'); + expect(res.hour).to.eql(1); + expect(res.min).to.eql(62.1); + expect(res.sec).to.eql(3723); + expect(res.msec).to.eql(3723004); + expect(res.toString()).to.eql('1h'); + }); + + describe('errors', () => { + it('should throw an error for invalid time format with missing parts', () => { + expect(() => Timestamp.parse('12:34')).to.throw('Invalid time format'); + }); + + it('should throw an error for invalid seconds/milliseconds format', () => { + expect(() => Timestamp.parse('12:34:56')).to.throw('Invalid seconds/milliseconds format'); + + // This test expects that an empty millisecond part will eventually result in an invalid number error. + expect(() => Timestamp.parse('12:34:56.')).to.throw('Invalid number in timestamp'); + }); + + it('should throw an error for non-numeric values', () => { + expect(() => Timestamp.parse('aa:bb:cc.ddd')).to.throw('Invalid number in timestamp'); + }); + }); + }); + + describe('parse: timestamp map ā { "HH:MM:SS.mmm": T }', () => { + it('should return an empty [array] when given an empty object', () => { + const test = (input?: any) => { + expect(Timestamp.parse(input)).to.eql([]); + }; + test({}); + test(undefined); + test(null); + }); + + it('should ensure "00:00:00.000" entry', () => { + const a = Timestamp.parse({}); + const b = Timestamp.parse({}, { ensureZero: true }); + expect(a).to.eql([]); + + expect(b.length).to.eql(1); + expect(b[0].timestamp).to.eql('00:00:00.000'); + expect(b[0].data).to.eql({}); + expect(b[0].total.msec).to.eql(0); + }); + + it('should correctly parse a single timestamp', () => { + const input: MyTimestamps = { + '00:01:02.003': { image: 'single' }, + }; + const result = Timestamp.parse(input); + expect(result).to.have.lengthOf(1); + const parsed = result[0]; + + // The original timestamp should be preserved. + expect(parsed.timestamp).to.eql('00:01:02.003'); + + // Calculate expected parsed values. + // For "00:01:02.003": + // hours: 0, minutes: 1, seconds: 2, milliseconds: 3 + const expectedMsecs = 0 * 3600000 + 1 * 60000 + 2 * 1000 + 3; // 62003 msecs + const expectedSecs = expectedMsecs / 1000; + const expectedMins = expectedMsecs / 60000; + const expectedHours = expectedMsecs / 3600000; + + expect(parsed.total.msec).to.eql(expectedMsecs); + expect(parsed.total.sec).to.eql(Num.round(expectedSecs, 1)); + expect(parsed.total.min).to.eql(Num.round(expectedMins, 1)); + expect(parsed.total.hour).to.eql(Num.round(expectedHours, 1)); + + // The associated data should be preserved. + expect(parsed.data).to.eql({ image: 'single' }); + }); + + it('should correctly parse and sort multiple timestamps', () => { + const input: MyTimestamps = { + '00:00:10.000': { image: 'second' }, + '00:00:15.000': { image: 'third' }, + '00:00:05.000': { image: 'first' }, + }; + const result = Timestamp.parse(input); + expect(result).to.have.lengthOf(3); + + // Expect the results to be sorted by time in ascending order. + expect(result[0].timestamp).to.eql('00:00:05.000'); + expect(result[1].timestamp).to.eql('00:00:10.000'); + expect(result[2].timestamp).to.eql('00:00:15.000'); + + // Validate the parsed milliseconds for the first timestamp. + expect(result[0].total.msec).to.eql(5000); + expect(result[0].data).to.eql({ image: 'first' }); + }); + + it('should throw an error if one of the timestamps has an invalid format', () => { + const input: MyTimestamps = { + '00:00:05.000': { image: 'first' }, + invalid: { image: 'broken' }, + }; + expect(() => Timestamp.parse(input)).to.throw('Invalid time format'); + }); + }); + + describe('find', () => { + const timestamps: MyTimestamps = { + '00:00:10.000': { image: 'second' }, + '00:00:15.000': { image: 'third' }, + '00:00:05.000': { image: 'first' }, + }; + + it('should return [undefined] if elapsed time is before the first timestamp', () => { + // Elapsed time of 2 seconds is less than the first timestamp (5 seconds) + const res = Timestamp.find(timestamps, 2_000); + expect(res).to.be.undefined; + }); + + it('should return the first timestamp when elapsed equals its time', () => { + // Elapsed time exactly 5 seconds. + const res = Timestamp.find(timestamps, 5_000); + expect(res?.data).to.eql({ image: 'first' }); + }); + + it('should return the correct timestamp when elapsed falls between two timestamps', () => { + // Elapsed time 7 seconds: the only timestamp <= 7 seconds is the first (5 seconds). + const a = Timestamp.find(timestamps, 7_000); + expect(a?.data).to.eql({ image: 'first' }); + + // Elapsed time 12 seconds: the latest timestamp with time <= 12 seconds is the second (10 seconds). + const b = Timestamp.find(timestamps, 12_000); + expect(b?.data).to.eql({ image: 'second' }); + }); + + it('should return the last timestamp if elapsed time exceeds all timestamps', () => { + // Elapsed time 20 seconds: all timestamps are <= 20 seconds, so it returns the last one. + const res = Timestamp.find(timestamps, 20_000); + expect(res?.data).to.eql({ image: 'third' }); + }); + + it('option: should use seconds (instead of milliseconds) as time comparison unit', () => { + const a = Timestamp.find(timestamps, 12_000); + const b = Timestamp.find(timestamps, 12, { unit: 'secs' }); + const c = Timestamp.find(timestamps, 12); // expect failure. + expect(a?.data).to.eql({ image: 'second' }); + expect(a?.data).to.eql(b?.data); + expect(c).to.not.eql(a); + }); + }); + + describe('isCurrent', () => { + const timestamps: MyTimestamps = { + '00:00:00.000': { image: 'image-0.png' }, + '00:00:05.000': { image: 'image-5.png' }, + '00:00:10.000': { image: 'image-10.png' }, + }; + + it('should return [false] when current time is before the first timestamp', () => { + // When current time is less than the first timestamp, no candidate is found. + expect(Timestamp.isCurrent(timestamps, '00:00:00.000', -1)).to.be.false; + }); + + it('should return [true] when current time equals the first timestamp', () => { + // currentTime is exactly at the first timestamp. + expect(Timestamp.isCurrent(timestamps, '00:00:00.000', 0)).to.be.true; + }); + + it('should return [false] if the candidate timestamp does not match the provided timestamp', () => { + // For currentTime 3 sec, the candidate should be '00:00:00.000', not '00:00:05.000' + expect(Timestamp.isCurrent(timestamps, '00:00:05.000', 3_000)).to.be.false; + }); + + it('should return [true] when current time exactly matches a later timestamp', () => { + // For currentTime 5 sec, the candidate should be '00:00:05.000' + expect(Timestamp.isCurrent(timestamps, '00:00:05.000', 5_000)).to.be.true; + }); + + it('should return [true] when current time is between two timestamps and candidate remains unchanged', () => { + // For currentTime 7 sec, candidate remains '00:00:05.000' + expect(Timestamp.isCurrent(timestamps, '00:00:05.000', 7_000)).to.be.true; + }); + + it('should return [false] when the provided timestamp is not present in the timestamps', () => { + // Provided timestamp does not exist in the timestamps object. + expect(Timestamp.isCurrent(timestamps, '00:00:20.000', 7_000)).to.be.false; + }); + + it('option: should use seconds (instead of milliseconds) as time comparison unit', () => { + const a = Timestamp.isCurrent(timestamps, '00:00:05.000', 5_000); + const b = Timestamp.isCurrent(timestamps, '00:00:05.000', 5, { unit: 'secs' }); + const c = Timestamp.isCurrent(timestamps, '00:00:05.000', 5); // expect failure. + expect(a).to.be.true; + expect(b).to.be.true; + expect(c).to.not.eql(a); + }); + }); + + describe('toString', () => { + it('should format 0 milliseconds as "00:00:00.000"', () => { + const ts = Timestamp.parse('00:00:00.000'); + expect(Timestamp.toString(ts)).to.eql('00:00:00.000'); + expect(Timestamp.toString('0:0:0.0')).to.eql('00:00:00.000'); + }); + + it('should format 123 milliseconds as "00:00:00.123"', () => { + const ts = '00:00:00.123'; + expect(Timestamp.toString(ts)).to.eql(ts); + }); + + it('should format 1000 milliseconds as "00:00:01.000"', () => { + const ts = '00:00:01.000'; + expect(Timestamp.toString(ts)).to.eql(ts); + }); + + it('should format 61234 milliseconds as "00:01:01.234"', () => { + // 61234 ms = 0 hours, 1 minute, 1 second, 234 ms + const duration = Time.Duration.create(61234); + expect(Timestamp.toString(duration)).to.eql('00:01:01.234'); + }); + + it('should format a duration with days and hours correctly', () => { + // Example: 1 day, 7 hours, 6 minutes, 45 seconds, and 123 milliseconds. + // 1 day = 86,400,000 ms + // 7 hours = 7 * 3,600,000 = 25,200,000 ms + // 6 minutes = 6 * 60,000 = 360,000 ms + // 45 seconds = 45 * 1,000 = 45,000 ms + // Total = 86,400,000 + 25,200,000 + 360,000 + 45,000 + 123 = 112,005,123 ms. + const totalMsec = 112005123; + const duration = Time.Duration.create(totalMsec); + expect(Timestamp.toString(duration)).to.eql('31:06:45.123'); + }); + }); + + describe('range', () => { + const timestamps: MyTimestamps = { + '00:00:10.000': { image: 'second' }, + '00:00:15.000': { image: 'third' }, + '00:00:05.000': { image: 'first' }, + }; + + describe('response: undefined', () => { + it('empty {timestamps}', () => { + const range = Timestamp.range({}, 0); + expect(range).to.eql(undefined); + }); + + it('out of range', () => { + const range = Timestamp.range(timestamps, 99, { unit: 'secs' }); + expect(range).to.eql(undefined); + }); + }); + + describe('create', () => { + it('from inferred 0-point', () => { + const range = Timestamp.range(timestamps, 3_000); + expect(range?.start).to.eql('00:00:00.000'); + expect(range?.end).to.eql('00:00:05.000'); + }); + + it('between to timestamps', () => { + const range = Timestamp.range(timestamps, 6_000); + expect(range?.start).to.eql('00:00:05.000'); + expect(range?.end).to.eql('00:00:10.000'); + }); + + it('from "timestamp" string', () => { + const range = Timestamp.range(timestamps, '00:00:06.001'); + expect(range?.start).to.eql('00:00:05.000'); + expect(range?.end).to.eql('00:00:10.000'); + }); + }); + + describe('progress (percentage)', () => { + it('should calculate progress correctly at the bounds', () => { + const res = Timestamp.range(timestamps, 7000); + expect(res).to.not.be.undefined; + if (res) { + // At the start of the range (5000 msec), progress should be 0. + expect(res.progress(5000)).to.equal(0); + // At the end of the range (10000 msec), progress should be 1. + expect(res.progress(10000)).to.equal(1); + } + }); + + it('should calculate progress correctly for an intermediate value', () => { + const res = Timestamp.range(timestamps, 7000); + expect(res).to.not.be.undefined; + + // For a progress time of 7500 msec, progress = (7500 - 5000) / (10000 - 5000) = 0.5 + expect(res?.progress(7500)).to.equal(0.5); + }); + + it('should respect unit conversion for progress calculations', () => { + // Use seconds instead of milliseconds. + // The location is 7 seconds (7000 msec). In the sorted timestamps, + // the range remains from 5000 msec to 10000 msec. + const res = Timestamp.range(timestamps, 7, { unit: 'secs' }); + expect(res).to.not.be.undefined; + + // When passing times in seconds, progress(5) should be calculated as 5000 msec, so progress = 0. + expect(res?.progress(5, { unit: 'secs' })).to.equal(0); + + // progress(10) (i.e. 10000 msec) should equal 1. + expect(res?.progress(10, { unit: 'secs' })).to.equal(1); + + // progress(7.5) seconds (7500 msec) should equal 0.5. + expect(res?.progress(7.5, { unit: 'secs' })).to.equal(0.5); + }); + + it('should round progress correctly when the round option is provided', () => { + // Use a round option (e.g., 2 decimal places) and choose a time that results in a non-integer fraction. + const res = Timestamp.range(timestamps, 7000, { round: 2 }); + expect(res).to.not.be.undefined; + + // For a progress time of 6666 msec, the unrounded progress is: + // (6666 - 5000) / (10000 - 5000) = 1666 / 5000 ā 0.3332 + // With rounding to 2 decimals, we expect ā 0.33. + const prog = res?.progress(6666); + expect(prog).to.be.closeTo(0.33, 0.01); + }); + }); + }); +}); diff --git a/code/sys/std/src/m.DateTime/-Until.-unit.test.ts b/code/sys/std/src/m.DateTime/-Until.test.ts similarity index 71% rename from code/sys/std/src/m.DateTime/-Until.-unit.test.ts rename to code/sys/std/src/m.DateTime/-Until.test.ts index 9e93d8892f..a7816878ef 100644 --- a/code/sys/std/src/m.DateTime/-Until.-unit.test.ts +++ b/code/sys/std/src/m.DateTime/-Until.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from '../-test.ts'; +import { type t, describe, expect, it } from '../-test.ts'; import { rx } from '../m.Rx/mod.ts'; import { Time } from './mod.ts'; @@ -33,7 +33,6 @@ describe('Time.until', () => { time.dispose$.subscribe(() => disposeFired++); expect(time.disposed).to.eql(false); - const res = time.delay(10, () => (count += 1)); dispose(); @@ -43,5 +42,22 @@ describe('Time.until', () => { expect(time.disposed).to.eql(true); expect(disposeFired).to.eql(1); }); + + it('constructs from ({ lifecycle/disposable }) object', async () => { + const test = async (input: t.Disposable) => { + let count = 0; + + const time = Time.until(input.dispose$); + const res = time.delay(10, () => (count += 1)); + input.dispose(); + + await res; + await Time.wait(20); + expect(count).to.eql(0); + }; + + await test(rx.lifecycle()); + await test(rx.disposable()); + }); }); }); diff --git a/code/sys/std/src/m.DateTime/m.Time.delay.ts b/code/sys/std/src/m.DateTime/m.Time.delay.ts index 4d7cf69e10..80c5c521eb 100644 --- a/code/sys/std/src/m.DateTime/m.Time.delay.ts +++ b/code/sys/std/src/m.DateTime/m.Time.delay.ts @@ -12,29 +12,47 @@ export function delay(...args: any[]): t.TimeDelayPromise { const { signal } = controller; const cancel = () => controller.abort('delay cancelled'); - const is: t.DeepMutable<T['is']> = { completed: false, cancelled: false, done: false }; + const is: t.DeepMutable<T['is']> = { done: false, completed: false, cancelled: false }; + let promise: any; - const promise: any = new Promise<void>((resolve) => { - const done = () => { - is.done = true; - is.cancelled = signal.aborted; - resolve(); - }; + if (msecs === undefined) { + /** + * Micro-task queue ("tick"). + */ + promise = Promise.resolve(); + promise + .then(() => { + fn?.(); + is.completed = true; + is.done = true; + }) + .catch(() => (is.done = true)); + } else { + /** + * Macro-task queue. + */ + promise = new Promise<void>((resolve) => { + const done = () => { + is.done = true; + is.cancelled = signal.aborted; + resolve(); + }; - signal.onabort = done; - const complete = () => { - fn?.(); - is.completed = true; - done(); - }; + signal.onabort = done; + const complete = () => { + fn?.(); + is.completed = true; + done(); + }; - denoDelay(msecs, { signal }).then(complete).catch(done); - }); + denoDelay(msecs, { signal }).then(complete).catch(done); + }); + } // Decorate the promise with extra time/delay controller fields. promise.cancel = cancel; promise.is = is; - promise.timeout = msecs; + promise.timeout = msecs || 0; return promise as T; } @@ -43,7 +61,7 @@ export function delay(...args: any[]): t.TimeDelayPromise { */ export const Wrangle = { delayArgs(input: any[]) { - let msecs = 0; + let msecs = undefined; let fn: t.TimeDelayCallback | undefined; if (typeof input[0] === 'number') msecs = input[0]; if (typeof input[0] === 'function') fn = input[0]; diff --git a/code/sys/std/src/m.DateTime/m.Time.until.ts b/code/sys/std/src/m.DateTime/m.Time.until.ts index 72b13f0b8c..9ac96078f7 100644 --- a/code/sys/std/src/m.DateTime/m.Time.until.ts +++ b/code/sys/std/src/m.DateTime/m.Time.until.ts @@ -2,36 +2,39 @@ import type { t } from './common.ts'; import { Subject, take, takeUntil } from 'rxjs'; import { Dispose } from '../m.Dispose/mod.ts'; -import { delay as baseDelay, Wrangle as DelayWrangle } from './m.Time.delay.ts'; +import { delay, Wrangle } from './m.Time.delay.ts'; /** * Exposes timer functions that cease after a dispose signal is received. */ export function until(until$: t.UntilObservable) { const life = Dispose.lifecycle(until$); - const { dispose$ } = life; const api: t.TimeUntil = { delay(...args: any[]): t.TimeDelayPromise { - const { msecs, fn } = DelayWrangle.delayArgs(args); + const { msecs, fn } = Wrangle.delayArgs(args); const done$ = new Subject<void>(); - const res = baseDelay(msecs, () => { + const res = delay(msecs, () => { done$.next(); return fn?.(); }); + life.dispose$.pipe(takeUntil(done$), take(1)).subscribe(() => res.cancel()); return res; }, wait(msecs) { - return api.delay(msecs ?? 0); + return typeof msecs === 'number' ? api.delay(msecs) : api.delay(); }, /** * Lifecycle */ - dispose$, + dispose: life.dispose, + get dispose$() { + return life.dispose$; + }, get disposed() { return life.disposed; }, diff --git a/code/sys/std/src/m.DateTime/m.Time.wait.ts b/code/sys/std/src/m.DateTime/m.Time.wait.ts index 0c1d27d316..fb9ec7ea66 100644 --- a/code/sys/std/src/m.DateTime/m.Time.wait.ts +++ b/code/sys/std/src/m.DateTime/m.Time.wait.ts @@ -6,6 +6,6 @@ import { delay } from './m.Time.delay.ts'; * (NB: use with `await`.) * @param msecs: delay in milliseconds. */ -export const wait: t.TimeLib['wait'] = (msecs = 0) => { +export const wait: t.TimeLib['wait'] = (msecs) => { return delay(msecs); }; diff --git a/code/sys/std/src/m.DateTime/m.Timestamp.parse.ts b/code/sys/std/src/m.DateTime/m.Timestamp.parse.ts new file mode 100644 index 0000000000..a6c8f53526 --- /dev/null +++ b/code/sys/std/src/m.DateTime/m.Timestamp.parse.ts @@ -0,0 +1,71 @@ +import { type t, isRecord } from './common.ts'; +import { Duration } from './m.Time.Duration.ts'; + +/** + * Parse a "HH:MM:DD:mmm" string into a structured object. + */ +export function parseTime( + timestamp: t.StringTimestamp, + options: { round?: number } = {}, +): t.TimeDuration { + // Split the timestamp into "hour", "minute", and "second.millisecond" parts. + const parts = timestamp.split(':'); + if (parts.length !== 3) throw new Error(`Invalid time format: ${timestamp}`); + + // Parse hours and minutes. + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + + // Split the seconds and milliseconds (expected format "SS.mmm"). + const timeParts = parts[2].split('.'); + if (timeParts.length !== 2) { + throw new Error(`Invalid seconds/milliseconds format: ${parts[2]}`); + } + const seconds = parseInt(timeParts[0], 10); + const milliseconds = parseInt(timeParts[1], 10); + + // Check that all parts are valid numbers. + if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) || isNaN(milliseconds)) { + throw new Error(`Invalid number in timestamp: ${timestamp}`); + } + + // Calculate the total milliseconds. + const msecs = + hours * 3_600_000 + // 1 hour = 3,600,000 msecs + minutes * 60_000 + // 1 minute = 60,000 msecs + seconds * 1_000 + // 1 second = 1,000 msecs + milliseconds; + + const { round } = options; + return Duration.create(msecs, { round }); +} + +/** + * Convert the set of { "HH:MM:SS:mmm":<value> } timestamp + * strings into list of sorted stuctures. + */ +export function parseMap<T>( + timestamps: t.Timestamps<T>, + options: { round?: number; ensureZero?: boolean } = {}, +): t.Timestamp<T>[] { + if (!isRecord(timestamps)) return []; + + const ZERO = '00:00:00.000'; + if (options.ensureZero && !timestamps[ZERO]) { + timestamps = { ...timestamps, [ZERO]: {} as T }; + } + + const parse = (timestamp: string, data: T) => { + const total = parseTime(timestamp, options); + return { + timestamp, + data, + get total() { + return total; + }, + }; + }; + return Object.entries(timestamps) + .map(([timestamp, data]) => parse(timestamp, data)) + .sort((a, b) => a.total.sec - b.total.sec); +} diff --git a/code/sys/std/src/m.DateTime/m.Timestamp.range.ts b/code/sys/std/src/m.DateTime/m.Timestamp.range.ts new file mode 100644 index 0000000000..106ae95fbc --- /dev/null +++ b/code/sys/std/src/m.DateTime/m.Timestamp.range.ts @@ -0,0 +1,53 @@ +import { type t } from './common.ts'; +import { parseMap, parseTime } from './m.Timestamp.parse.ts'; + +/** + * Generate a sub-range for a timestamp within a map of timestamps. + */ +export const range: t.TimestampLib['range'] = (timestamps, location, options = {}) => { + const { unit = 'msecs', round } = options; + const map = parseMap(timestamps, { round, ensureZero: true }); + const msecs = wrangle.msecs(location, unit); + + // Ensure there are enough timestamps. + if (map.length < 2) return undefined; + + // Iterate over the sorted timestamps to find where currentTime fits. + for (let i = 0; i < map.length - 1; i++) { + const start = map[i].total.msec; + const end = map[i + 1].total.msec; + + if (msecs >= start && msecs <= end) { + const api: t.TimestampRange = { + start: map[i].timestamp, + end: map[i + 1].timestamp, + progress(time, opt = {}) { + const progressTime = wrangle.msecs(time, opt.unit ?? unit, round); + let prog = (progressTime - start) / (end - start); + if (round !== undefined) { + const factor = Math.pow(10, round); + prog = Math.round(prog * factor) / factor; + } + return prog as t.Percent; + }, + }; + return api; + } + } + + // Out-of-range. + return undefined; +}; + +/** + * Helpers + */ +const wrangle = { + msecs(input: t.NumberTime | t.StringTimestamp, unit: t.TimestampUnit, round?: number): t.Msecs { + if (typeof input === 'string') { + return parseTime(input, { round }).msec; + } else { + return unit === 'secs' ? input * 1000 : input; + } + }, +} as const; diff --git a/code/sys/std/src/m.DateTime/m.Timestamp.ts b/code/sys/std/src/m.DateTime/m.Timestamp.ts new file mode 100644 index 0000000000..9c0916e951 --- /dev/null +++ b/code/sys/std/src/m.DateTime/m.Timestamp.ts @@ -0,0 +1,89 @@ +import { type t, isRecord } from './common.ts'; +import { parseMap, parseTime } from './m.Timestamp.parse.ts'; +import { range } from './m.Timestamp.range.ts'; + +/** + * Tools for working with timestamps ("HH:MM:SS.mmm"). + */ +export const Timestamp: t.TimestampLib = { + range, + + parse(input: any, options: any) { + if (input === undefined || input === null) return []; + if (isRecord(input)) return parseMap(input, options) as any; // NB: type-hack. + if (typeof input === 'string') return parseTime(input); + throw new Error(`Input type not supported: ${typeof input}`); + }, + + find<T>( + timestamps: t.Timestamps<T>, + time: t.Msecs, + options: { unit?: t.TimestampUnit; round?: number } = {}, + ): t.Timestamp<T> | undefined { + const { unit, round } = options; + const msecs = wrangle.msecs(time, { unit }); + const parsedTimes = parseMap(timestamps, { round }); + let candidate: t.Timestamp<T> | undefined = undefined; + for (const entry of parsedTimes) { + // Match the last timestamp with time <= elapsed. + if (entry.total.msec <= msecs) candidate = entry; + else break; + } + return candidate; + }, + + isCurrent<T>( + timestamps: t.Timestamps<T>, + timestamp: t.StringTimestamp, + current: t.Secs, + options: { unit?: t.TimestampUnit; round?: number } = {}, + ) { + const { unit, round } = options; + const msecs = wrangle.msecs(current, { unit }); + const parsedTimes = parseMap(timestamps, { round }); + let candidate: t.Timestamp<T> | undefined = undefined; + + // Find the last timestamp with total time <= currentTime. + for (const entry of parsedTimes) { + if (entry.total.msec <= msecs) { + candidate = entry; + } else { + break; + } + } + + return candidate ? candidate.timestamp === timestamp : false; + }, + + toString(input) { + const duration = isRecord(input) ? input : Timestamp.parse(input); + const msec = duration.msec; + + // Calculate hours, minutes, seconds and milliseconds from the total milliseconds. + const hours = Math.floor(msec / 3600000); + const remainderAfterHours = msec % 3600000; + const minutes = Math.floor(remainderAfterHours / 60000); + const remainderAfterMinutes = remainderAfterHours % 60000; + const seconds = Math.floor(remainderAfterMinutes / 1000); + const milliseconds = remainderAfterMinutes % 1000; + + // Format with leading zeros. + const hoursStr = hours.toString().padStart(2, '0'); + const minutesStr = minutes.toString().padStart(2, '0'); + const secondsStr = seconds.toString().padStart(2, '0'); + const millisStr = milliseconds.toString().padStart(3, '0'); + + return `${hoursStr}:${minutesStr}:${secondsStr}.${millisStr}`; + }, +}; + +/** + * Helpers + */ +const wrangle = { + msecs(value: number, options: { unit?: t.TimestampUnit } = {}) { + const { unit = 'msecs' } = options; + if (unit === 'secs') value = value * 1000; + return value; + }, +} as const; diff --git a/code/sys/std/src/m.DateTime/mod.ts b/code/sys/std/src/m.DateTime/mod.ts index 75fd14b657..e5a4f2ec84 100644 --- a/code/sys/std/src/m.DateTime/mod.ts +++ b/code/sys/std/src/m.DateTime/mod.ts @@ -26,6 +26,7 @@ * elapsed.toString('m'); // ā "210m" * ``` */ -export { Date, Date as D, Day, Format } from './m.Date.ts'; +export { Date as D, Date, Day, Format } from './m.Date.ts'; export { Duration } from './m.Time.Duration.ts'; export { Time } from './m.Time.ts'; +export { Timestamp } from './m.Timestamp.ts'; diff --git a/code/sys/std/src/m.DateTime/t.Time.Timestamp.ts b/code/sys/std/src/m.DateTime/t.Time.Timestamp.ts new file mode 100644 index 0000000000..ef6028c8d3 --- /dev/null +++ b/code/sys/std/src/m.DateTime/t.Time.Timestamp.ts @@ -0,0 +1,64 @@ +import type { t } from './common.ts'; + +export type TimestampUnit = 'msecs' | 'secs'; + +/** + * Tools for working with timestamps ("HH:MM:SS.mmm"). + */ +export type TimestampLib = { + /** + * Convert a parsed [TimeDuration] back into a timestamp ("HH:MM:SS.mmm") + */ + toString(input: t.TimeDuration | t.StringTimestamp): string; + + /** + * Parse a "HH:MM:DD:mmm" string into a structured object. + */ + parse(timestamp: t.StringTimestamp, options?: { round?: number }): t.TimeDuration; + + /** + * Convert the map of { "HH:MM:SS:mmm": <T> } timestamps + * into a sorted list stuctured objects. + */ + parse<T>( + timestamps?: t.Timestamps<T>, + options?: { round?: number; ensureZero?: boolean }, + ): t.Timestamp<T>[]; + + /** + * Lookup a timestamp from an elapsed time within a {timestamps} map. + */ + find<T>( + timestamps: t.Timestamps<T>, + time: number, + options?: { unit?: t.TimestampUnit; round?: number }, + ): t.Timestamp<T> | undefined; + + /** + * Check if a given timestamp is the current one within a set based on the given time. + */ + isCurrent<T>( + timestamps: t.Timestamps<T>, + timestamp: t.StringTimestamp, + time: t.NumberTime, + options?: { unit?: t.TimestampUnit; round?: number }, + ): boolean; + + /** + * Generate a sub-range for a timestamp within a map of timestamps. + */ + range<T>( + timestamps: t.Timestamps<T>, + location: t.NumberTime | t.StringTimestamp, + options?: { unit?: t.TimestampUnit; round?: number }, + ): TimestampRange | undefined; +}; + +/** + * A timestamp range (start/end). + */ +export type TimestampRange = { + readonly start: t.StringTimestamp; + readonly end: t.StringTimestamp; + progress(time: t.NumberTime, options?: { unit?: t.TimestampUnit }): t.Percent; +}; diff --git a/code/sys/std/src/m.DateTime/t.Time.ts b/code/sys/std/src/m.DateTime/t.Time.ts index 33ebbf507a..5e3daf809d 100644 --- a/code/sys/std/src/m.DateTime/t.Time.ts +++ b/code/sys/std/src/m.DateTime/t.Time.ts @@ -36,7 +36,7 @@ export type TimeLib = { utc(input?: t.DateTimeInput): t.DateTime; /** A Time helper that runs only until it has been disposed. */ - until(until$?: t.UntilObservable): t.TimeUntil; + until(until$?: t.DisposeInput): t.TimeUntil; }; /** @@ -71,17 +71,11 @@ export type TimeDelay = { /** * Exposes timer functions that cease after a dispose signal is received. */ -export type TimeUntil = { - /** Fires when the transient time helper is disposed. */ - readonly dispose$: t.Observable<void>; - - /** Flag indicating if the transient time heper is disposed. */ - readonly disposed: boolean; - +export type TimeUntil = t.Lifecycle & { /** Delay for the specified milliseconds. */ delay: t.TimeLib['delay']; - /** Wait for the specified milliseconds pass. */ + /** Wait for the specified milliseconds to pass. */ wait: t.TimeLib['wait']; }; diff --git a/code/sys/std/src/m.DateTime/t.ts b/code/sys/std/src/m.DateTime/t.ts index d897477a39..f9ef061209 100644 --- a/code/sys/std/src/m.DateTime/t.ts +++ b/code/sys/std/src/m.DateTime/t.ts @@ -1,3 +1,4 @@ export type * from './t.Date.ts'; -export type * from './t.Time.ts'; export type * from './t.Time.Duration.ts'; +export type * from './t.Time.Timestamp.ts'; +export type * from './t.Time.ts'; diff --git a/code/sys/std/src/m.Dispose/-.test.ts b/code/sys/std/src/m.Dispose/-.test.ts index 50fd0a6a14..65cfd396b4 100644 --- a/code/sys/std/src/m.Dispose/-.test.ts +++ b/code/sys/std/src/m.Dispose/-.test.ts @@ -1,12 +1,13 @@ import { Subject } from 'rxjs'; import { describe, expect, it, type t } from '../-test.ts'; import { Time } from '../m.DateTime/mod.ts'; +import { Is, rx } from '../mod.ts'; import { Dispose } from './mod.ts'; describe('Disposable', () => { describe('sync', () => { it('disposable: create ā dispose', () => { - const test = (until?: t.UntilObservable) => { + const test = (until?: t.DisposeInput) => { const obj = Dispose.disposable(until); let count = 0; @@ -14,7 +15,12 @@ describe('Disposable', () => { obj.dispose(); obj.dispose(); - until?.forEach((subject) => subject.next()); + + if (Is.disposable(until)) { + until?.dispose(); + } else { + until?.forEach((subject) => subject.next()); + } expect(count).to.eql(1); }; @@ -22,10 +28,13 @@ describe('Disposable', () => { test(); test(new Subject<void>()); test([new Subject<void>(), new Subject<void>()]); + + test(rx.disposable()); + test(rx.lifecycle()); }); it('lifecycle: create ā dispose', () => { - const test = (until?: t.UntilObservable) => { + const test = (until?: t.DisposeInput) => { const obj = Dispose.lifecycle(until); expect(obj.disposed).to.eql(false); @@ -34,13 +43,22 @@ describe('Disposable', () => { obj.dispose(); obj.dispose(); - until?.forEach((subject) => subject.next()); + + if (Is.disposable(until)) { + until?.dispose(); + } else { + until?.forEach((subject) => subject.next()); + } + expect(count).to.eql(1); // NB: multiple calls to dispose to not refire the cleanup handler. }; test(); test(new Subject<void>()); test([new Subject<void>(), new Subject<void>()]); + + test(rx.disposable()); + test(rx.lifecycle()); }); }); @@ -215,14 +233,24 @@ describe('Disposable', () => { }); describe('Dispose.until', () => { - it('single', () => { + it('Input: single (observable)', () => { const $ = new Subject<void>(); const res = Dispose.until($); expect(res.length).to.eql(1); expect(res[0]).to.equal($); }); - it('list', () => { + it('Input: <t.Disposable>', () => { + const test = (input: t.Disposable) => { + const res = Dispose.until(input); + expect(res.length).to.eql(1); + expect(res[0]).to.equal(input.dispose$); + }; + test(rx.disposable()); + test(rx.lifecycle()); + }); + + it('Input: list', () => { const $1 = new Subject<void>(); const $2 = new Subject<void>(); const res = Dispose.until([$1, undefined, $2]); @@ -232,7 +260,7 @@ describe('Disposable', () => { expect(res[1]).to.eql($2); }); - it('deep list ā flattens', () => { + it('Input: deep list ā flattens', () => { const $1 = new Subject<void>(); const $2 = new Subject<void>(); const res = Dispose.until([$1, undefined, [undefined, [undefined, $2]]]); @@ -257,4 +285,59 @@ describe('Disposable', () => { expect(count).to.eql(1); }); }); + + describe('Dispose.toLifecycle', () => { + it('lifecycle', () => { + type T = t.Lifecycle & { count: number }; + const life = rx.lifecycle(); + const api = Dispose.toLifecycle<T>(life, { count: 123 }); + + let fired = 0; + api.dispose$.subscribe(() => fired++); + + expect(api.count).to.eql(123); + expect(api.disposed).to.eql(false); + + life.dispose(); + api.dispose(); + expect(fired).to.eql(1); + expect(api.disposed).to.eql(true); + }); + + it('(no param) generate lifecycle', () => { + type T = t.Lifecycle & { count: number }; + const api = Dispose.toLifecycle<T>({ count: 123 }); + let fired = 0; + api.dispose$.subscribe(() => fired++); + + expect(api.count).to.eql(123); + expect(api.disposed).to.eql(false); + + api.dispose(); + expect(fired).to.eql(1); + expect(api.disposed).to.eql(true); + }); + }); + + describe('Dispose.omitDispose', () => { + type T = t.Lifecycle & { count: number }; + + it('from lifecycle', () => { + const life = Dispose.lifecycle(); + const a = Dispose.toLifecycle<T>(life, { count: 123 }); + expect(typeof a.dispose === 'function').to.be.true; + + const b = Dispose.omitDispose(a); + expect(a).to.not.equal(b); + expect((b as any).dispose === undefined).to.be.true; + + let fired = 0; + b.dispose$.subscribe(() => fired++); + + expect(b.disposed).to.eql(false); + life.dispose(); + expect(b.disposed).to.eql(true); + expect(fired).to.eql(1); + }); + }); }); diff --git a/code/sys/std/src/m.Dispose/common.ts b/code/sys/std/src/m.Dispose/common.ts index 246d5f3cce..5adc5e3a35 100644 --- a/code/sys/std/src/m.Dispose/common.ts +++ b/code/sys/std/src/m.Dispose/common.ts @@ -1,4 +1,7 @@ export { flatten } from 'rambda'; -export { Subject, filter, take } from 'rxjs'; +export { filter, Subject, take } from 'rxjs'; export * from '../common.ts'; +export { Delete } from '../m.Delete/mod.ts'; +export { Err } from '../m.Err/mod.ts'; +export { Is } from '../m.Is/mod.ts'; diff --git a/code/sys/std/src/m.Dispose/m.Dispose.ts b/code/sys/std/src/m.Dispose/m.Dispose.ts index aec2b6e214..74190bc74e 100644 --- a/code/sys/std/src/m.Dispose/m.Dispose.ts +++ b/code/sys/std/src/m.Dispose/m.Dispose.ts @@ -1,138 +1,24 @@ -import { Delete } from '../m.Delete/mod.ts'; -import { Err } from '../m.Err/mod.ts'; -import { Is } from '../m.Rx/m.Is.ts'; -import { Subject, filter, flatten, take, type t } from './common.ts'; +import { type t } from './common.ts'; + +import { disposable, disposableAsync } from './u.dispose.ts'; +import { done } from './u.done.ts'; +import { lifecycle, lifecycleAsync, toLifecycle } from './u.lifecycle.ts'; +import { until } from './u.until.ts'; +import { omitDispose } from './u.omitDispose.ts'; /** * Toolkit for working with disposable interfaces. */ export const Dispose: t.DisposeLib = { - /** - * Generates a generic disposable interface that is - * typically mixed into a wider interface of some kind. - */ - disposable(until$): t.Disposable { - const dispose$ = new Subject<void>(); - const disposable: t.Disposable = { - dispose$: dispose$.asObservable(), - dispose: () => Dispose.done(dispose$), - }; - Dispose.until(until$).forEach(($) => $.subscribe(disposable.dispose)); - return disposable; - }, - - /** - * Generates an asnchronous Disposable interface. - */ - disposableAsync(...args: any[]) { - const { until$, onDispose } = wrangle.disposableAsyncArgs(args); - const dispose$ = new Subject<t.DisposeAsyncEvent>(); - let _disposing = false; - - type P = t.DisposeAsyncEventArgs; - const asPayload = (stage: t.DisposeAsyncStage, error?: t.DisposeError): P => { - const ok = !error; - const done = stage === 'complete' || stage === 'error'; - return Delete.undefined({ stage, is: { ok, done }, error }); - }; - const fire = (stage: t.DisposeAsyncStage, error?: t.DisposeError) => { - const payload = asPayload(stage, error); - dispose$.next({ type: 'dispose', payload }); - }; - - const disposable: t.DisposableAsync = { - dispose$: dispose$.asObservable(), - async dispose() { - if (_disposing) return; - _disposing = true; - - fire('start'); - try { - await onDispose?.(); // Invoke handler ("clean up resources"). - fire('complete'); - } catch (err: any) { - fire('error', { - name: 'DisposeError', - message: 'Failed while disposing asynchronously', - cause: Err.std(err), - }); - } - }, - }; - - Dispose.until(until$).forEach(($) => $.subscribe(disposable.dispose)); - return disposable; - }, + done, + until, - /** - * Generates a disposable interface that maintains - * and exposes it's disposed state. - */ - lifecycle(until$) { - const { dispose, dispose$ } = Dispose.disposable(until$); - let _disposed = false; - dispose$.pipe(take(1)).subscribe(() => (_disposed = true)); - return { - dispose$, - dispose, - get disposed() { - return _disposed; - }, - }; - }, + disposable, + disposableAsync, - lifecycleAsync(...args) { - const { until$, onDispose } = wrangle.disposableAsyncArgs(args); - const { dispose, dispose$ } = Dispose.disposableAsync(until$, onDispose); - let _disposed = false; - dispose$ - .pipe( - filter((e) => e.payload.stage === 'complete' || e.payload.stage === 'error'), - take(1), - ) - .subscribe(() => (_disposed = true)); - return { - dispose$, - dispose, - get disposed() { - return _disposed; - }, - }; - }, + lifecycle, + lifecycleAsync, + toLifecycle, - /** - * Listens to an observable (or set of observbles) and - * disposes of the target when any of them fire. - */ - until($) { - const list = Array.isArray($) ? $ : [$]; - return flatten(list).filter(Boolean) as t.Observable<unknown>[]; - }, - - /** - * "Completes" a subject by running: - * 1. subject.next(); - * 2. subject.complete(); - */ - done(dispose$) { - dispose$?.next?.(); - dispose$?.complete?.(); - }, + omitDispose, }; - -/** - * Helpers - */ -export const wrangle = { - disposableAsyncArgs(args: any[]) { - type Fn = () => Promise<void>; - let onDispose: Fn | undefined; - let until$: t.UntilObservable | undefined; - - if (typeof args[0] === 'function') onDispose = args[0]; - if (typeof args[1] === 'function') onDispose = args[1]; - if (Is.observable(args[0]) || Array.isArray(args[0])) until$ = Dispose.until(args[0]); - - return { onDispose, until$ }; - }, -} as const; diff --git a/code/sys/std/src/m.Dispose/t.ts b/code/sys/std/src/m.Dispose/t.ts index 0c559d656b..20a5a47227 100644 --- a/code/sys/std/src/m.Dispose/t.ts +++ b/code/sys/std/src/m.Dispose/t.ts @@ -11,7 +11,7 @@ export type DisposeLib = { * Generates a generic disposable interface that is * typically mixed into a wider interface of some kind. */ - disposable(until$?: t.UntilObservable): t.Disposable; + disposable(until$?: t.DisposeInput): t.Disposable; /** An async variant of the dispose pattern. */ disposableAsync(onDispose?: t.LifecycleStageHandler): t.DisposableAsync; @@ -21,16 +21,20 @@ export type DisposeLib = { * Generates a disposable interface that maintains * and exposes it's disposed state. */ - lifecycle(until$?: t.UntilObservable): t.Lifecycle; + lifecycle(until$?: t.DisposeInput): t.Lifecycle; /** An async variant of the lifecycle pattern. */ lifecycleAsync(onDispose?: LifecycleStageHandler): t.LifecycleAsync; - lifecycleAsync(until$?: t.UntilObservable, onDispose?: LifecycleStageHandler): t.LifecycleAsync; + lifecycleAsync(until$?: t.DisposeInput, onDispose?: LifecycleStageHandler): t.LifecycleAsync; + + /** Extend the given object to be expose the lifecycle API. */ + toLifecycle<T extends t.Lifecycle>(life: t.Lifecycle, api: t.OmitLifecycle<T>): T; + toLifecycle<T extends t.Lifecycle>(api: t.OmitLifecycle<T>): T; /** * Listens to an observable and disposes of the object when fires. */ - until($?: t.UntilObservable): t.Observable<unknown>[]; + until(dispose$?: t.DisposeInput): t.Observable<unknown>[]; /** * "Completes" a subject by running: @@ -39,4 +43,11 @@ export type DisposeLib = { * 2. subject.complete(); */ done(dispose$?: t.Subject<void>): void; + + /** + * Safely remove the `dispose` method from a disposable. + * NB: useful for surfacing from an API where you don't want + * callers to be able to disose of the resource. + */ + omitDispose<T extends t.Disposable | t.DisposableAsync>(obj: T): Omit<T, 'dispose'>; }; diff --git a/code/sys/std/src/m.Dispose/u.dispose.ts b/code/sys/std/src/m.Dispose/u.dispose.ts new file mode 100644 index 0000000000..be59215d6e --- /dev/null +++ b/code/sys/std/src/m.Dispose/u.dispose.ts @@ -0,0 +1,78 @@ +import { type t, Delete, Err, Is, Subject } from './common.ts'; +import { done } from './u.done.ts'; +import { until } from './u.until.ts'; + +/** + * Generates a generic disposable interface that is + * typically mixed into a wider interface of some kind. + */ +export function disposable(until$?: t.UntilInput) { + const subject$ = new Subject<void>(); + const dispose$ = subject$.asObservable(); + const disposable: t.Disposable = { + dispose: () => done(subject$), + get dispose$() { + return dispose$; + }, + }; + until(until$).forEach(($) => $.subscribe(disposable.dispose)); + return disposable; +} + +/** + * Generates an asnchronous Disposable interface. + */ +export function disposableAsync(...args: any[]) { + const { until$, onDispose } = toDisposableAsyncArgs(args); + const dispose$ = new Subject<t.DisposeAsyncEvent>(); + let _disposing = false; + + type P = t.DisposeAsyncEventArgs; + const asPayload = (stage: t.DisposeAsyncStage, error?: t.DisposeError): P => { + const ok = !error; + const done = stage === 'complete' || stage === 'error'; + return Delete.undefined({ stage, is: { ok, done }, error }); + }; + const fire = (stage: t.DisposeAsyncStage, error?: t.DisposeError) => { + const payload = asPayload(stage, error); + dispose$.next({ type: 'dispose', payload }); + }; + + const disposable: t.DisposableAsync = { + dispose$: dispose$.asObservable(), + async dispose() { + if (_disposing) return; + _disposing = true; + + fire('start'); + try { + await onDispose?.(); // Invoke handler ("clean up resources"). + fire('complete'); + } catch (err: any) { + fire('error', { + name: 'DisposeError', + message: 'Failed while disposing asynchronously', + cause: Err.std(err), + }); + } + }, + }; + + until(until$).forEach(($) => $.subscribe(disposable.dispose)); + return disposable; +} + +/** + * Helpers + */ +export function toDisposableAsyncArgs(args: any[]) { + type Fn = () => Promise<void>; + let onDispose: Fn | undefined; + let until$: t.UntilObservable | undefined; + + if (typeof args[0] === 'function') onDispose = args[0]; + if (typeof args[1] === 'function') onDispose = args[1]; + if (Is.observable(args[0]) || Array.isArray(args[0])) until$ = until(args[0]); + + return { onDispose, until$ }; +} diff --git a/code/sys/std/src/m.Dispose/u.done.ts b/code/sys/std/src/m.Dispose/u.done.ts new file mode 100644 index 0000000000..f45413a6cc --- /dev/null +++ b/code/sys/std/src/m.Dispose/u.done.ts @@ -0,0 +1,11 @@ +import { type t } from './common.ts'; + +/** + * "Completes" a subject by running: + * 1. subject.next(); + * 2. subject.complete(); + */ +export function done(dispose$?: t.Subject<void>) { + dispose$?.next?.(); + dispose$?.complete?.(); +} diff --git a/code/sys/std/src/m.Dispose/u.lifecycle.ts b/code/sys/std/src/m.Dispose/u.lifecycle.ts new file mode 100644 index 0000000000..2a08f1ff3e --- /dev/null +++ b/code/sys/std/src/m.Dispose/u.lifecycle.ts @@ -0,0 +1,81 @@ +import { type t, filter, take } from './common.ts'; +import { disposable, disposableAsync, toDisposableAsyncArgs } from './u.dispose.ts'; + +type L = t.Lifecycle; + +/** + * Generates a disposable interface that maintains + * and exposes it's disposed state. + */ +export function lifecycle(until$?: t.UntilInput) { + const { dispose, dispose$ } = disposable(until$); + let _disposed = false; + dispose$.pipe(take(1)).subscribe(() => (_disposed = true)); + return { + dispose, + get dispose$() { + return dispose$; + }, + get disposed() { + return _disposed; + }, + }; +} + +/** + * An async variant of the lifecycle pattern. + */ +export function lifecycleAsync(...args: any[]) { + const { until$, onDispose } = toDisposableAsyncArgs(args); + const { dispose, dispose$ } = disposableAsync(until$, onDispose); + let _disposed = false; + dispose$ + .pipe( + filter((e) => e.payload.stage === 'complete' || e.payload.stage === 'error'), + take(1), + ) + .subscribe(() => (_disposed = true)); + return { + dispose$, + dispose, + get disposed() { + return _disposed; + }, + }; +} + +/** + * Extend the given object to be expose the lifecycle API. + */ +export const toLifecycle: t.DisposeLib['toLifecycle'] = <T extends L>(...input: any[]): T => { + const { api, life } = wrangle.toLifecycleParams(input); + const obj = api as T & L; + + Object.defineProperties(obj, { + dispose: { + value: life.dispose.bind(life), + enumerable: true, + }, + dispose$: { + get: () => life.dispose$, + enumerable: true, + }, + disposed: { + get: () => life.disposed, + enumerable: true, + }, + }); + + return obj; +}; + +/** + * Helpers + */ +const wrangle = { + toLifecycleParams<T extends t.Lifecycle>(input: any[]): { life: t.Lifecycle; api: T } { + if (input.length === 1) return { life: lifecycle(), api: input[0] as T }; + if (input.length >= 2) return { life: input[0], api: input[1] }; + throw new Error('Failed to parse overloads: toLifecycle'); + }, +} as const; diff --git a/code/sys/std/src/m.Dispose/u.omitDispose.ts b/code/sys/std/src/m.Dispose/u.omitDispose.ts new file mode 100644 index 0000000000..4ad046b895 --- /dev/null +++ b/code/sys/std/src/m.Dispose/u.omitDispose.ts @@ -0,0 +1,21 @@ +import { type t } from './common.ts'; + +type D = t.Disposable | t.DisposableAsync; + +/** + * Safely remove the `dispose` method from a disposable. + * NB: useful for surfacing from an API where you don't want + * callers to be able to disose of the resource. + */ +export function omitDispose<T extends D>(obj: T): Omit<T, 'dispose'> { + const proto = Object.getPrototypeOf(obj); + const allDescs = Object.getOwnPropertyDescriptors(obj); + const newDescs: PropertyDescriptorMap = {}; + + for (const [key, desc] of Object.entries(allDescs)) { + if (key === 'dispose') continue; // NB: skip it. + newDescs[key] = desc; + } + + return Object.create(proto, newDescs) as Omit<T, 'dispose'>; +} diff --git a/code/sys/std/src/m.Dispose/u.until.ts b/code/sys/std/src/m.Dispose/u.until.ts new file mode 100644 index 0000000000..e4fd10efd4 --- /dev/null +++ b/code/sys/std/src/m.Dispose/u.until.ts @@ -0,0 +1,15 @@ +import { type t, flatten, Is } from './common.ts'; + +/** + * Listens to an observable (or set of observbles) and + * disposes of the target when any of them fire. + */ +export function until(input?: t.DisposeInput) { + if (Is.disposable(input)) { + return [input.dispose$]; + } else { + const $ = input; + const list = Array.isArray($) ? $ : [$]; + return flatten(list).filter(Boolean) as t.Observable<unknown>[]; + } +} diff --git a/code/sys/std/src/m.Err/-Err.catch.test.ts b/code/sys/std/src/m.Err/-Err.tryCatch.test.ts similarity index 88% rename from code/sys/std/src/m.Err/-Err.catch.test.ts rename to code/sys/std/src/m.Err/-Err.tryCatch.test.ts index 5af3425b1a..61e1f0c0d1 100644 --- a/code/sys/std/src/m.Err/-Err.catch.test.ts +++ b/code/sys/std/src/m.Err/-Err.tryCatch.test.ts @@ -9,7 +9,7 @@ describe('Err.catch', () => { }; it('success', async () => { - const res = await Err.catch(getUser(1)); + const res = await Err.tryCatch(getUser(1)); expect(res.ok).to.eql(true); expect(res.data).to.eql({ id: 1 }); expect(res.data?.id).to.equal(1); @@ -17,7 +17,7 @@ describe('Err.catch', () => { }); it('fail (error)', async () => { - const res = await Err.catch(getUser(123, true)); + const res = await Err.tryCatch(getUser(123, true)); expect(res.ok).to.eql(false); expect(res.data).to.eql(undefined); diff --git a/code/sys/std/src/m.Err/m.Err.ts b/code/sys/std/src/m.Err/m.Err.ts index 3f1bb9911b..660fffa82d 100644 --- a/code/sys/std/src/m.Err/m.Err.ts +++ b/code/sys/std/src/m.Err/m.Err.ts @@ -3,7 +3,7 @@ import { Is } from './m.Is.ts'; import { Name } from './m.Name.ts'; import { errors } from './u.errors.ts'; import { std } from './u.std.ts'; -import { catchError } from './u.catchError.ts'; +import { tryCatch } from './u.tryCatchError.ts'; /** * Helpers for working with errors. @@ -13,5 +13,5 @@ export const Err: t.ErrLib = { Name, std, errors, - catch: catchError, + tryCatch, }; diff --git a/code/sys/std/src/m.Err/t.ts b/code/sys/std/src/m.Err/t.ts index 4375774264..fc487f181d 100644 --- a/code/sys/std/src/m.Err/t.ts +++ b/code/sys/std/src/m.Err/t.ts @@ -20,19 +20,19 @@ export type ErrLib = { errors(): t.ErrorCollection; /** - * Principled way to handle try/catch/(error) execution - * on async functions avoiding the proliferation of - * try/catch statements. + * Function strongly typed way to handle try/catch (error) + * execution on async functions avoiding the proliferation of + * problematic native try/catch statements around a codebase. */ - catch<T>(promise: Promise<T>): Promise<t.ErrCatch<T>>; + tryCatch<T>(promise: Promise<T>): Promise<t.ErrCatch<T>>; }; /** * The response (and/or error) from an [Err.catch] method call. */ -export type ErrCatch<T> = ErrCatchSuccess<T> | ErrCatchFail<T>; -export type ErrCatchSuccess<T> = { ok: true; data: T; error: undefined }; -export type ErrCatchFail<T> = { ok: false; data?: T; error: t.StdError }; +export type ErrCatch<T> = ErrSuccess<T> | ErrFail<T>; +export type ErrSuccess<T> = { ok: true; data: T; error: undefined }; +export type ErrFail<T> = { ok: false; data?: T; error: t.StdError }; /** * Options passed to the `ErrLib.stdErr` method. diff --git a/code/sys/std/src/m.Err/u.catchError.ts b/code/sys/std/src/m.Err/u.tryCatchError.ts similarity index 71% rename from code/sys/std/src/m.Err/u.catchError.ts rename to code/sys/std/src/m.Err/u.tryCatchError.ts index a7a64abad0..34c68f62fc 100644 --- a/code/sys/std/src/m.Err/u.catchError.ts +++ b/code/sys/std/src/m.Err/u.tryCatchError.ts @@ -1,9 +1,7 @@ import type { t } from './common.ts'; import { std } from './u.std.ts'; -type F = t.ErrLib['catch']; - -export const catchError: F = async <T>(promise: Promise<T>) => { +export const tryCatch: t.ErrLib['tryCatch'] = async <T>(promise: Promise<T>) => { try { const data = await promise; return { ok: true, data, error: undefined }; diff --git a/code/sys/std/src/m.Immutable/-T.example.test.ts b/code/sys/std/src/m.Immutable/-T.example.test.ts new file mode 100644 index 0000000000..17bef78232 --- /dev/null +++ b/code/sys/std/src/m.Immutable/-T.example.test.ts @@ -0,0 +1,94 @@ +import { type t, describe, expect, it, slug } from '../-test.ts'; +import { single } from 'rxjs'; +import { Immutable } from './mod.ts'; + +describe('T:Immutable', () => { + /** + * Types: + */ + type P = t.PatchOperation; + + type MyState = { tmp: number }; + type MyStateEvent = t.InferImmutableEvent<MyStateEvents>; + type MyStateEvents = t.ImmutableEvents<MyState, P>; + type MyStateImmutable = t.ImmutableRef<MyState, P, MyStateEvents>; + + /** + * Sample implementation: + */ + const DEFAULT_ID = `default:${slug()}`; + let refs: Map<string, MyStateImmutable>; + const reset = () => (refs = new Map<string, MyStateImmutable>()); + reset(); + + const factory = (instanceId: string = DEFAULT_ID) => { + if (refs.has(instanceId)) return refs.get(instanceId)!; + + const model = Immutable.clonerRef<MyState>({ tmp: 0 }); + refs.set(instanceId, model); + return model; + }; + + const Store = { + state: factory, + }; + + /** + * State Factory (instantiation): + */ + it('example type declaration: /t.ts', () => { + const singleton = Store.state(); + const another = Store.state('something else'); + expect(singleton).to.equal(Store.state()); // NB: no-param ā singleton factory. + expect(singleton).to.not.equal(another); + + /** + * Immutable change: + */ + singleton.change((d) => d.tmp++); + expect(singleton.current.tmp).to.eql(1); + expect(another.current.tmp).to.eql(0); + }); + + describe('Store.state: (sample function)', () => { + it('default (singleton)', () => { + reset(); + const a = Store.state(); + const b = Store.state(); + expect(a).to.equal(b); // NB: same instance + }); + + it('custom: instance-id', () => { + reset(); + const id = 'foo'; + const a = Store.state(id); + const b = Store.state(id); + const c = Store.state(); + const d = Store.state('something else'); + expect(a).to.equal(b); // NB: same instance + expect(a).to.not.equal(c); + expect(a).to.not.equal(d); + }); + + it('change', () => { + reset(); + const a = Store.state(); + const b = Store.state(); + expect(a.current.tmp).to.eql(0); + a.change((d) => d.tmp++); + expect(b.current.tmp).to.eql(1); + }); + + it('shared events', () => { + reset(); + const a = Store.state(); + const b = Store.state(); + const bEvents = a.events(); + const bFired: MyStateEvent[] = []; + bEvents.changed$.pipe().subscribe((e) => bFired.push(e)); + + a.change((d) => d.tmp++); + expect(bFired[0].after).to.eql(a.current); + }); + }); +}); diff --git a/code/sys/std/src/m.Immutable/Immutable.events.ts b/code/sys/std/src/m.Immutable/Immutable.events.ts index 7d503a32bf..d0f8ffe33a 100644 --- a/code/sys/std/src/m.Immutable/Immutable.events.ts +++ b/code/sys/std/src/m.Immutable/Immutable.events.ts @@ -13,17 +13,11 @@ type DefaultPatch = t.PatchOperation; */ export function viaObservable<T, P = DefaultPatch>( $: t.Observable<t.ImmutableChange<T, P>>, - dispose$?: t.UntilObservable, + dispose$?: t.UntilInput, ): t.ImmutableEvents<T, P> { const life = rx.lifecycle(dispose$); - return { - changed$: $.pipe(rx.takeUntil(life.dispose$)), - dispose: life.dispose, - dispose$: life.dispose$, - get disposed() { - return life.disposed; - }, - }; + const changed$ = $.pipe(rx.takeUntil(life.dispose$)); + return rx.toLifecycle<t.ImmutableEvents<T, P>>(life, { changed$ }); } /** @@ -32,7 +26,7 @@ export function viaObservable<T, P = DefaultPatch>( */ export function viaOverride<T, P = DefaultPatch>( source: t.Immutable<T, P>, - dispose$?: t.UntilObservable, + dispose$?: t.UntilInput, ): t.ImmutableEvents<T, P> { const $ = rx.subject<t.ImmutableChange<T, P>>(); const api = viaObservable<T, P>($, dispose$); diff --git a/code/sys/std/src/m.Immutable/t.ts b/code/sys/std/src/m.Immutable/t.ts index 0a1134b478..a4278d7f71 100644 --- a/code/sys/std/src/m.Immutable/t.ts +++ b/code/sys/std/src/m.Immutable/t.ts @@ -17,12 +17,12 @@ type ClonerRef = <T>( type EventsViaOverride = <T, P = t.PatchOperation>( source: t.Immutable<T, P>, - dispose$?: t.UntilObservable, + dispose$?: t.UntilInput, ) => t.ImmutableEvents<T, P>; type EventsViaObservable = <T, P = t.PatchOperation>( $: t.Observable<t.ImmutableChange<T, P>>, - dispose$?: t.UntilObservable, + dispose$?: t.UntilInput, ) => t.ImmutableEvents<T, P>; /** diff --git a/code/sys/std/src/m.Is/-.test.ts b/code/sys/std/src/m.Is/-.test.ts index 1f5932b45c..806b263a2c 100644 --- a/code/sys/std/src/m.Is/-.test.ts +++ b/code/sys/std/src/m.Is/-.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '../-test.ts'; -import { Err, Is, Rx } from '../mod.ts'; +import { Err, Is, rx, Rx } from '../mod.ts'; describe('Is (common flags)', () => { it('rx: observable | subject', () => { @@ -139,6 +139,20 @@ describe('Is (common flags)', () => { }); }); + describe('Is.uint8Array', () => { + const binary = new Uint8Array([1, 2, 3]); + + it('is not Uint8Array', () => { + const NON = ['', 123, true, null, undefined, BigInt(0), Symbol('foo'), {}, []]; + NON.forEach((v) => expect(Is.uint8Array(v)).to.eql(false)); + expect(Is.uint8Array(binary.buffer)).to.eql(false); + }); + + it('is Uint8Array', () => { + expect(Is.uint8Array(binary)).to.equal(true); + }); + }); + describe('Is.blank', () => { describe('blank', () => { it('is blank (nothing)', () => { @@ -194,11 +208,38 @@ describe('Is (common flags)', () => { }); }); - it('Is.statusOK', () => { - const NON = ['foo', 123, false, null, undefined, {}, [], Symbol('foo'), BigInt(0)]; - NON.forEach((v: any) => expect(Is.statusOK(v)).to.eql(false)); - expect(Is.statusOK(200)).to.eql(true); - expect(Is.statusOK(201)).to.eql(true); - expect(Is.statusOK(404)).to.eql(false); + describe('Is.statusOK', () => { + it('Is.statusOK: true', () => { + expect(Is.statusOK(200)).to.eql(true); + expect(Is.statusOK(201)).to.eql(true); + }); + + it('Is.statusOK: false', () => { + const NON = ['foo', 123, false, null, undefined, {}, [], Symbol('foo'), BigInt(0)]; + NON.forEach((v: any) => expect(Is.statusOK(v)).to.eql(false)); + expect(Is.statusOK(404)).to.eql(false); + }); + }); + + describe('Is.browser', () => { + it('Is.browser: false', () => { + expect(Is.browser()).to.eql(false); + }); + }); + + describe('Is.disposable', () => { + it('Is.disposable: true', () => { + const disposable = rx.disposable(); + const life = rx.lifecycle(); + expect(Is.disposable(disposable)).to.be.true; + expect(Is.disposable(life)).to.be.true; + }); + + it('Is.disposable: false', () => { + const NON = ['', 123, true, null, undefined, BigInt(0), Symbol('foo'), {}, []]; + NON.forEach((value) => { + expect(Is.disposable(value)).to.eql(false); + }); + }); }); }); diff --git a/code/sys/std/src/m.Is/m.Is.ts b/code/sys/std/src/m.Is/m.Is.ts index ec347ec59a..3181610c2c 100644 --- a/code/sys/std/src/m.Is/m.Is.ts +++ b/code/sys/std/src/m.Is/m.Is.ts @@ -1,19 +1,35 @@ import { type t, isObject } from '../common.ts'; import { Err } from '../m.Err/mod.ts'; -import { Is as RxIs } from '../m.Rx/mod.ts'; -const { observable, subject } = RxIs; const { errorLike, stdError } = Err.Is; /** * Common flag evaluators. */ export const Is: t.StdIsLib = { - observable, - subject, errorLike, stdError, + disposable(input?: any): input is t.Disposable { + if (!isObject(input)) return false; + const obj = input as t.Disposable; + return typeof obj.dispose === 'function' && Is.observable(obj.dispose$); + }, + + /** + * Determine if the given input is an Observable. + */ + observable<T = unknown>(input?: any): input is t.Observable<T> { + return typeof input === 'object' && typeof input?.subscribe === 'function'; + }, + + /** + * Determine if the given input is an observable Subject. + */ + subject<T = unknown>(input?: any): input is t.Subject<T> { + return Is.observable(input) && typeof (input as any)?.next === 'function'; + }, + falsy(input?: any): input is t.Falsy | typeof NaN { return ( input === false || @@ -61,7 +77,12 @@ export const Is: t.StdIsLib = { }, arrayBufferLike(input?: any): input is ArrayBufferLike { - return input instanceof ArrayBuffer || input instanceof SharedArrayBuffer; + const tag = Object.prototype.toString.call(input); + return tag === '[object ArrayBuffer]' || tag === '[object SharedArrayBuffer]'; + }, + + uint8Array(input?: any): input is Uint8Array { + return Object.prototype.toString.call(input) === '[object Uint8Array]'; }, /** @@ -97,4 +118,12 @@ export const Is: t.StdIsLib = { if (typeof input !== 'number') return false; return String(input)[0] === '2'; }, + + /** + * Determines if currently running within a browser environment. + */ + browser() { + const g = globalThis; + return typeof g.window === 'object' && typeof g.document === 'object'; + }, }; diff --git a/code/sys/std/src/m.Is/t.ts b/code/sys/std/src/m.Is/t.ts index 58f4ab6b8a..133941f1e5 100644 --- a/code/sys/std/src/m.Is/t.ts +++ b/code/sys/std/src/m.Is/t.ts @@ -15,6 +15,11 @@ export type StdIsLib = { */ nil(input?: unknown): boolean; + /** + * Determine if the input is an object implementing the <t.Disposable> interface. + */ + disposable(input?: unknown): input is t.Disposable; + /** * Determine if the value is a Promise. */ @@ -23,12 +28,12 @@ export type StdIsLib = { /** * Determine if the value is an observable Subject. */ - subject: t.RxIs['subject']; + subject<T = unknown>(input?: any): input is t.Subject<T>; /** * Determine if the value is an Observable. */ - observable: t.RxIs['observable']; + observable<T = unknown>(input?: any): input is t.Observable<T>; /** * Determine if the value is like an Error object. @@ -51,10 +56,15 @@ export type StdIsLib = { json(input?: unknown): input is t.Json; /** - * Determine if the input is ArrayBufferLike. + * Determine if the input is [ArrayBufferLike]. */ arrayBufferLike(input?: unknown): input is ArrayBufferLike; + /** + * Determine if the inut is a [Uint8Array]. + */ + uint8Array(input?: unknown): input is Uint8Array; + /** * A safe way to test any value as to wheather is is 'blank' * meaning it can be either: @@ -70,4 +80,7 @@ export type StdIsLib = { /** Determine if the HTTP status code is within the 200 range. */ statusOK(status: number): boolean; + + /** Determines if currently running within a browser environment. */ + browser(): boolean; }; diff --git a/code/sys/std/src/m.Pkg/-Pkg.Dist.test.ts b/code/sys/std/src/m.Pkg/-Pkg.Dist.test.ts index 66d11be773..d5d53e6347 100644 --- a/code/sys/std/src/m.Pkg/-Pkg.Dist.test.ts +++ b/code/sys/std/src/m.Pkg/-Pkg.Dist.test.ts @@ -12,6 +12,7 @@ describe('Pkg.Dist', () => { const SAMPLE = { dist(): t.DistPkg { return { + '-type:': 'jsr:@sys/types:DistPkg', pkg: { name: `@ns/foo-${slug()}`, version: '1.2.3' }, size: { bytes: 1234 }, entry: './main.js', diff --git a/code/sys/std/src/m.Pkg/-Pkg.test.ts b/code/sys/std/src/m.Pkg/-Pkg.test.ts index f585d5b865..b84c885094 100644 --- a/code/sys/std/src/m.Pkg/-Pkg.test.ts +++ b/code/sys/std/src/m.Pkg/-Pkg.test.ts @@ -91,6 +91,7 @@ describe('Pkg', () => { it('true', () => { const dist: t.DistPkg = { + '-type:': 'jsr:@sys/types:DistPkg', pkg: { name: 'foo', version: '1.2.3' }, size: { bytes: 123_456 }, entry: 'pkg/entry.js', diff --git a/code/sys/std/src/m.Random/.test.ts b/code/sys/std/src/m.Random/.test.ts index f1107dd124..535f1597bf 100644 --- a/code/sys/std/src/m.Random/.test.ts +++ b/code/sys/std/src/m.Random/.test.ts @@ -13,7 +13,7 @@ describe('Random', () => { }); describe('Random.base36: string ā [0-9] and [A-Z]', () => { - const test = (length: number, total = 1000) => { + const test = (length: number, total = 100) => { const list = Array.from({ length: total }).map(() => Random.base36(length)); expect(R.uniq(list)).to.eql(list); // NB: no repeating random numbers. list.forEach((value) => expect(value.length).to.eql(length)); diff --git a/code/sys/std/src/m.Rx/-Rx.test.ts b/code/sys/std/src/m.Rx/-Rx.test.ts index 1d5204827e..802f3ef569 100644 --- a/code/sys/std/src/m.Rx/-Rx.test.ts +++ b/code/sys/std/src/m.Rx/-Rx.test.ts @@ -1,7 +1,15 @@ -import { describe, expect, it, Time, Testing } from '../-test.ts'; +import { describe, expect, it, Testing, Time } from '../-test.ts'; import { Dispose, rx, Rx } from '../mod.ts'; describe('Observable/rx', () => { + describe('API', () => { + expect(rx.toLifecycle).to.equal(Dispose.toLifecycle); + expect(rx.lifecycle).to.equal(Dispose.lifecycle); + expect(rx.lifecycleAsync).to.equal(Dispose.lifecycleAsync); + expect(rx.disposable).to.equal(Dispose.disposable); + expect(rx.disposableAsync).to.equal(Dispose.disposableAsync); + }); + it('dual cased names', () => { expect(Rx).to.equal(rx); }); diff --git a/code/sys/std/src/m.Rx/common.ts b/code/sys/std/src/m.Rx/common.ts index 903a07f5e9..3f144d4ef8 100644 --- a/code/sys/std/src/m.Rx/common.ts +++ b/code/sys/std/src/m.Rx/common.ts @@ -1,3 +1,5 @@ export * from '../common.ts'; -export * from './u.Rx.libs.ts'; export { Dispose } from '../m.Dispose/mod.ts'; +export { Is as StdIs } from '../m.Is/mod.ts'; + +export * from './u.Rx.libs.ts'; diff --git a/code/sys/std/src/m.Rx/m.Is.ts b/code/sys/std/src/m.Rx/m.Is.ts index f6a7ab2512..7ede83f475 100644 --- a/code/sys/std/src/m.Rx/m.Is.ts +++ b/code/sys/std/src/m.Rx/m.Is.ts @@ -1,4 +1,4 @@ -import type { t } from '../common.ts'; +import { t, StdIs } from './common.ts'; type Event = { type: string; payload: unknown }; @@ -6,19 +6,8 @@ type Event = { type: string; payload: unknown }; * Type guards (boolean evaluators). */ export const Is: t.RxIs = { - /** - * Determine if the given input is an Observable. - */ - observable<T = unknown>(input?: any): input is t.Observable<T> { - return typeof input === 'object' && typeof input?.subscribe === 'function'; - }, - - /** - * Determine if the given input is an observable Subject. - */ - subject<T = unknown>(input?: any): input is t.Subject<T> { - return Is.observable(input) && typeof (input as any)?.next === 'function'; - }, + observable: StdIs.observable, + subject: StdIs.subject, /** * Determine if the object structure matches that of the diff --git a/code/sys/std/src/m.Rx/m.Rx.ts b/code/sys/std/src/m.Rx/m.Rx.ts index aa30270d3a..f6831959f8 100644 --- a/code/sys/std/src/m.Rx/m.Rx.ts +++ b/code/sys/std/src/m.Rx/m.Rx.ts @@ -2,12 +2,12 @@ import { type t, Dispose } from './common.ts'; import * as lib from './u.Rx.libs.ts'; import { Is } from './m.Is.ts'; +import { bus } from './u.bus.ts'; import { event, payload } from './u.payload.ts'; import { asPromise } from './u.promise.ts'; import { withinTimeThreshold } from './u.time.ts'; -import { bus } from './u.bus.ts'; -const { disposable, disposableAsync, lifecycle, lifecycleAsync, done } = Dispose; +const { disposable, disposableAsync, lifecycle, lifecycleAsync, done, toLifecycle } = Dispose; /** Tools for working with Observables (via the RXJS library). */ export const Rx: t.RxLib = { @@ -20,9 +20,10 @@ export const Rx: t.RxLib = { bus, done, disposable, - lifecycle, disposableAsync, + lifecycle, lifecycleAsync, + toLifecycle, event, payload, diff --git a/code/sys/std/src/m.Rx/t.ts b/code/sys/std/src/m.Rx/t.ts index 01b49c1dfe..dc3f8d895e 100644 --- a/code/sys/std/src/m.Rx/t.ts +++ b/code/sys/std/src/m.Rx/t.ts @@ -24,6 +24,7 @@ export type RxLib = Rxjs & { disposableAsync: t.DisposeLib['disposableAsync']; lifecycle: t.DisposeLib['lifecycle']; lifecycleAsync: t.DisposeLib['lifecycleAsync']; + toLifecycle: t.DisposeLib['toLifecycle']; withinTimeThreshold<T>( $: t.Observable<T>, @@ -56,8 +57,8 @@ export type RxPromiseResponse<E extends Event> = { */ export type RxIs = { event(input: any, type?: string | { startsWith: string }): boolean; - observable<T = unknown>(input?: any): input is t.Observable<T>; - subject<T = unknown>(input?: any): input is t.Subject<T>; + observable: t.StdIsLib['observable']; + subject: t.StdIsLib['subject']; }; /** diff --git a/code/sys/std/src/m.Rx/u.time.ts b/code/sys/std/src/m.Rx/u.time.ts index b0a249e865..7d545d0c6c 100644 --- a/code/sys/std/src/m.Rx/u.time.ts +++ b/code/sys/std/src/m.Rx/u.time.ts @@ -9,8 +9,10 @@ import { Subject, filter, take, takeUntil } from './u.Rx.libs.ts'; export function withinTimeThreshold<T>( $: t.Observable<T>, timeout: t.Msecs, - options: { dispose$?: t.UntilObservable } = {}, + options: { dispose$?: t.UntilInput } = {}, ): t.TimeThreshold<T> { + const life = Dispose.lifecycle(options.dispose$); + const listen = (timeout: number) => { type R = { result: boolean; value?: T }; const startedAt = Date.now(); @@ -36,32 +38,23 @@ export function withinTimeThreshold<T>( }; /** - * Response listener. + * Response listener: */ const timeout$ = new Subject<void>(); const $$ = new Subject<T>(); $.subscribe((e) => { const listen$ = listen(timeout).pipe( - takeUntil(dispose$), + takeUntil(life.dispose$), filter((e) => !!e.result), ); listen$.subscribe((e) => $$.next(e.value!)); }); - let _disposed = false; - const { dispose, dispose$ } = Dispose.disposable(options.dispose$); - dispose$.subscribe(() => { - $$.complete(); - _disposed = true; + /** + * API: + */ + return Dispose.toLifecycle<t.TimeThreshold<T>>(life, { + $: $$.pipe(takeUntil(life.dispose$)), + timeout$: timeout$.pipe(takeUntil(life.dispose$)), }); - - return { - $: $$.pipe(takeUntil(dispose$)), - timeout$: timeout$.pipe(takeUntil(dispose$)), - dispose, - dispose$, - get disposed() { - return _disposed; - }, - }; } diff --git a/code/sys/std/src/m.Signal/-.test.ts b/code/sys/std/src/m.Signal/-.test.ts new file mode 100644 index 0000000000..f4f1bac4d7 --- /dev/null +++ b/code/sys/std/src/m.Signal/-.test.ts @@ -0,0 +1,368 @@ +import { Time, describe, expect, it } from '../-test.ts'; +import { Signal } from './mod.ts'; +import { rx } from '../m.Rx/mod.ts'; + +import * as Preact from '@preact/signals-core'; + +describe('Signal', () => { + it('API', () => { + expect(Signal.create).to.equal(Preact.signal); + expect(Signal.effect).to.equal(Preact.effect); + }); + + describe('Core "Signal" API', () => { + describe('signal: update', () => { + it('should create a signal with an initial value and update correctly', () => { + const s = Signal.create(0); + expect(s.value).to.eql(0); + s.value = 42; + expect(s.value).to.eql(42); + }); + + it('updates object', async () => { + type T = { count: number }; + const initial = { count: 0 }; + const s = Signal.create<T>(initial); + expect(s.value).to.eql(initial); + expect(s.value).to.equal(initial); // NB: actual instance. + + let fired: T[] = []; + const stop = Signal.effect(() => { + fired.push(s.value); + }); + expect(fired).to.eql([initial]); + + // Replace object. + s.value = { count: 123 }; + + await Time.wait(); + expect(fired.length).to.eql(2); + expect(fired).to.eql([initial, { count: 123 }]); + + // Mutate object. + s.value.count = 456; + await Time.wait(); + + expect(s.value).to.eql({ count: 456 }); + expect(fired).to.eql([initial, { count: 456 }]); // ā ļø NB: the object IS NOT immutable. + expect(fired.length).to.eql(2); // ...and no change event was fired. + + stop(); + }); + }); + + describe('signal: effect (reactivity)', () => { + it('should run the effect whenever the signal value changes', async () => { + let dummy = 0; + const s = Signal.create(0); + + const stop = Signal.effect(() => { + dummy = s.value; + }); + + expect(dummy).to.eql(0); + s.value = 123; + + await Time.wait(); // NB: Wait for the effect to propagate (micro-task queue, aka. "tick"). + expect(dummy).to.eql(123); + + stop(); // NB: Stop the effect to release listener (dispose). + }); + }); + + describe('signal: computed', () => { + it('should create a derived signal that updates based on dependencies', () => { + const a = Signal.create(2); + const b = Signal.create(3); + const sum = Signal.computed(() => a.value + b.value); + expect(sum.value).to.eql(5); + + b.value = 10; + expect(sum.value).to.eql(12); + }); + }); + + describe('signal: batch (change)', () => { + it('should group updates so that effects run only once', () => { + let count = 0; + const x = Signal.create(1); + const y = Signal.create(2); + + Signal.effect(() => { + count++; + // NB: Access signals so this effect depends on them. + x.value; + y.value; + }); + + expect(count).to.eql(1); + + Signal.batch(() => { + x.value = 10; + y.value = 20; + }); + + // NB: Both updates happened in one batch, so the effect runs only once more. + expect(count).to.equal(2); + }); + }); + }); + + describe('value helpers', () => { + describe('Signal.toggle', () => { + it('should toggle boolean', () => { + const s = Signal.create(false); + expect(s.value).to.eql(false); + const res = Signal.toggle(s); + expect(s.value).to.eql(true); + expect(res).to.eql(true); + }); + + it('should toggle boolean from <undefined>', () => { + const s = Signal.create<boolean | undefined>(); + expect(s.value).to.eql(undefined); + const res = Signal.toggle(s); + expect(s.value).to.eql(true); + expect(res).to.eql(true); + Signal.toggle(s); + expect(s.value).to.eql(false); + Signal.toggle(s); + expect(s.value).to.eql(true); + }); + + it('force: true', () => { + const s = Signal.create(false); + const res1 = Signal.toggle(s, true); + const res2 = Signal.toggle(s, true); + expect(res1).to.eql(true); + expect(res2).to.eql(true); + expect(s.value).to.eql(true); + }); + + it('force: false', () => { + const s = Signal.create(true); + const res1 = Signal.toggle(s, false); + const res2 = Signal.toggle(s, false); + expect(res1).to.eql(false); + expect(res2).to.eql(false); + expect(s.value).to.eql(false); + }); + }); + + describe('Signal.cycle', () => { + type T = 'a' | 'b' | 'c'; + + it('should cycle union [string] signal', () => { + const s = Signal.create<T>('a'); + expect(s.value).to.eql('a'); + + const values: T[] = ['a', 'b', 'c']; + + // Cycle from "a" to "b". + const res1 = Signal.cycle(s, values); + expect(res1).to.eql('b'); + expect(s.value).to.eql('b'); + + // Cycle from "b" to "c". + const res2 = Signal.cycle(s, values); + expect(res2).to.eql('c'); + expect(s.value).to.eql('c'); + + // Cycle from "c" back to "a". + const res3 = Signal.cycle(s, values); + expect(res3).to.eql('a'); + expect(s.value).to.eql('a'); + }); + + it('cycles [number] array', () => { + const s = Signal.create<number>(1); + expect(s.value).to.eql(1); + + const values: number[] = [1, 2, 3]; + + // Cycle from 1 to 2. + const res1 = Signal.cycle(s, values); + expect(res1).to.eql(2); + expect(s.value).to.eql(2); + + // Cycle from 2 to 3. + const res2 = Signal.cycle(s, values); + expect(res2).to.eql(3); + expect(s.value).to.eql(3); + + // Cycle from 3 back to 1. + const res3 = Signal.cycle(s, values); + expect(res3).to.eql(1); + expect(s.value).to.eql(1); + }); + + it('cycles array of arrays (1)', () => { + type V = string | number; + const s = Signal.create<V[] | undefined>(); + expect(s.value).to.eql(undefined); + + const values: V[][] = [ + [1, 2], + [3, 4], + ['1fr', 100, 'auto'], + ]; + + const res = Signal.cycle(s, values); + expect(res).to.eql([1, 2]); + expect(s.value).to.eql([1, 2]); + + Signal.cycle(s, values); + expect(s.value).to.eql([3, 4]); + + Signal.cycle(s, values); + expect(s.value).to.eql(['1fr', 100, 'auto']); + }); + + it('cycle array of arrays (2)', () => { + type V = string | number; + type T = V | V[] | undefined; + const s = Signal.create<T>(); + const values = [undefined, 0, 10, [50, 15], ['1fr', 100, 'auto']]; + + const test = (expected: T) => { + Signal.cycle(s, values); + expect(s.value).to.eql(expected); + }; + + test(0); + test(10); + test([50, 15]); + test(['1fr', 100, 'auto']); + test(undefined); + }); + + it('cycles from <undefined>', () => { + const s = Signal.create<T | undefined>(); + expect(s.value).to.eql(undefined); + + const values: (T | undefined)[] = [undefined, 'a', 'b']; + const res = Signal.cycle(s, values); + expect(res).to.eql('a'); + expect(s.value).to.eql('a'); + + Signal.cycle(s, values); + expect(s.value).to.eql('b'); + + Signal.cycle(s, values); + expect(s.value).to.eql(undefined); + + Signal.cycle(s, values); + expect(s.value).to.eql('a'); + }); + + it('cycles from different initial value', () => { + const s = Signal.create<T>('b'); + expect(s.value).to.eql('b'); + + const values: T[] = ['a', 'b', 'c']; + const res = Signal.cycle(s, values); + expect(res).to.eql('c'); + expect(s.value).to.eql('c'); + }); + + it('cycles from <undefined> initial value', () => { + const s = Signal.create<T>(); + expect(s.value).to.eql(undefined); + + const values: T[] = ['a', 'b', 'c']; + const res1 = Signal.cycle<T>(s, values); + expect(res1).to.eql('a'); + expect(s.value).to.eql('a'); + + Signal.cycle<T>(s, values); + Signal.cycle<T>(s, values); + expect(s.value).to.eql('c'); + Signal.cycle<T>(s, values); + expect(s.value).to.eql('a'); + + // Edge-case: no values specified ā returns <undefined>. + s.value = undefined; + const res2 = Signal.cycle<T>(s, []); + expect(res2).to.eql(undefined); + expect(s.value).to.eql(undefined); + }); + + it('force value works', () => { + const s = Signal.create<T>('a'); + expect(s.value).to.eql('a'); + + const values: T[] = ['a', 'b', 'c']; + + // Force to "c" + const res1 = Signal.cycle(s, values, 'c'); + expect(res1).to.eql('c'); + expect(s.value).to.eql('c'); + + // Force to "b" + const res2 = Signal.cycle(s, values, 'b'); + expect(res2).to.eql('b'); + expect(s.value).to.eql('b'); + }); + + it('should default to first element if current value is not in values array', () => { + const s = Signal.create('z'); + expect(s.value).to.eql('z'); + + const values: T[] = ['a', 'b', 'c']; + + const res = Signal.cycle(s, values); + expect(res).to.eql('a'); + expect(s.value).to.eql('a'); + }); + }); + }); + + describe('Listeners', () => { + it('create ā <change> ā dispose', () => { + const life = rx.disposable(); + const a = Signal.listeners(); + const b = Signal.listeners(life); + + expect(a.disposed).to.eql(false); + expect(b.disposed).to.eql(false); + + expect(a.count).to.eql(0); + expect(b.count).to.eql(0); + + const signal = Signal.create(0); + const fired = { a: 0, b: 0 }; + const resA = a.effect(() => { + signal.value; + fired.a++; + }); + const resB = b.effect(() => { + signal.value; + fired.b++; + }); + + expect(resA.count).to.eql(1); + expect(resB.count).to.eql(1); + + expect(fired).to.eql({ a: 1, b: 1 }); // NB: initial run. + signal.value++; + expect(fired).to.eql({ a: 2, b: 2 }); + + life.dispose(); + expect(a.disposed).to.eql(false); + expect(b.disposed).to.eql(true); + expect(b.count).to.eql(0); + + signal.value++; + expect(fired).to.eql({ a: 3, b: 2 }); + + a.dispose(); + signal.value++; + expect(fired).to.eql({ a: 3, b: 2 }); // NB: no change. + + expect(a.disposed).to.eql(true); + expect(b.disposed).to.eql(true); + expect(a.count).to.eql(0); + expect(b.count).to.eql(0); + }); + }); +}); diff --git a/code/sys/std/src/m.Signal/common.ts b/code/sys/std/src/m.Signal/common.ts new file mode 100644 index 0000000000..579e1f4a09 --- /dev/null +++ b/code/sys/std/src/m.Signal/common.ts @@ -0,0 +1,2 @@ +export * from '../common.ts'; +export { Dispose } from '../m.Dispose/mod.ts'; diff --git a/code/sys/std/src/m.Signal/m.Signal.ts b/code/sys/std/src/m.Signal/m.Signal.ts new file mode 100644 index 0000000000..83437fd241 --- /dev/null +++ b/code/sys/std/src/m.Signal/m.Signal.ts @@ -0,0 +1,32 @@ +import { batch, computed, effect, signal } from '@preact/signals-core'; + +import type { t } from './common.ts'; +import { cycle } from './u.cycle.ts'; +import { toggle } from './u.toggle.ts'; +import { listeners } from './u.listeners.ts'; + +export { signal }; + +/** + * Reactive Signals. + * See: + * https://github.com/tc39/proposal-signals + * https://preactjs.com/blog/introducing-signals/ + * https://preactjs.com/guide/v10/signals + */ +export const Signal: t.SignalLib = { + /** + * Primary API: + */ + create: signal, + effect, + computed, + batch, + + /** + * Helpers: + */ + listeners, + toggle, + cycle, +} as const; diff --git a/code/sys/std/src/m.Signal/mod.ts b/code/sys/std/src/m.Signal/mod.ts new file mode 100644 index 0000000000..c75d94b0e9 --- /dev/null +++ b/code/sys/std/src/m.Signal/mod.ts @@ -0,0 +1,9 @@ +/** + * @module + * Reactive Signals. + * See: + * https://github.com/tc39/proposal-signals + * https://preactjs.com/blog/introducing-signals/ + * https://preactjs.com/guide/v10/signals + */ +export { Signal } from './m.Signal.ts'; diff --git a/code/sys/std/src/m.Signal/t.ts b/code/sys/std/src/m.Signal/t.ts new file mode 100644 index 0000000000..2d01dc195b --- /dev/null +++ b/code/sys/std/src/m.Signal/t.ts @@ -0,0 +1,64 @@ +import type { t } from './common.ts'; +import type Preact from '@preact/signals-core'; +import type { ReadonlySignal, Signal } from '@preact/signals-core'; + +export { ReadonlySignal, Signal }; + +/** + * Utility type to extract the type of a signal value. + * @example + * ```ts + * const mySignal = Signal.create<'Foo' | 'Bar'>('Foo'); + * type T = ExtractSignalValue<typeof mySignal>; + * ``` + */ +export type ExtractSignalValue<T> = T extends Signal<infer U> ? U : never; + +/** Callback passed into a signal effect. */ +export type SignalEffectFn = () => void | (() => void); + +/** + * Reactive Signals. + * See: + * https://github.com/tc39/proposal-signals + * https://preactjs.com/blog/introducing-signals/ + * https://preactjs.com/guide/v10/signals + */ +export type SignalLib = { + /** Create a new plain signal. */ + create: typeof Preact.signal; + + /** Create an effect to run arbitrary code in response to signal changes. */ + effect: typeof Preact.effect; + + /** Combine multiple value updates into one "commit" at the end of the provided callback. */ + batch: typeof Preact.batch; + + /** Create a new signal that is computed based on the values of other signals. */ + computed: typeof Preact.computed; + + /** Create a new listeners collection. */ + listeners(dispose$?: t.UntilInput): t.SignalListeners; + + // +} & t.SignalValueHelpersLib; + +/** + * Utility helpers for operating on Signal values. + */ +export type SignalValueHelpersLib = { + /** Toggle a boolean signal. */ + toggle(signal: Signal<boolean | undefined>, forceValue?: boolean): boolean; + + /** Cycle a union string signal through a list of possible values. */ + cycle<T>(signal: Signal<T | undefined>, values: T[], forceValue?: T): T; +}; + +/** + * Helper for managing the disposal of a collection + * of signal effect listeners. + */ +export type SignalListeners = t.Lifecycle & { + readonly count: number; + effect(fn: t.SignalEffectFn): SignalListeners; +}; diff --git a/code/sys/std/src/m.Signal/u.cycle.ts b/code/sys/std/src/m.Signal/u.cycle.ts new file mode 100644 index 0000000000..3ae6132ba9 --- /dev/null +++ b/code/sys/std/src/m.Signal/u.cycle.ts @@ -0,0 +1,24 @@ +import { type t, R } from './common.ts'; + +/** + * Cycle a union string signal through a list of possible values. + */ +export const cycle: t.SignalLib['cycle'] = <T>( + signal: t.Signal<T | undefined>, + values: T[], + forceValue?: T, +): T => { + const next = forceValue !== undefined ? forceValue : wrangle.next(signal, values); + signal.value = next; + return next; +}; + +/** + * Helpers + */ +const wrangle = { + next<T>(signal: t.Signal<T | undefined>, values: T[]): T { + const index = values.findIndex((item) => R.equals(item, signal.value)); + return values[(index + 1) % values.length]; + }, +} as const; diff --git a/code/sys/std/src/m.Signal/u.listeners.ts b/code/sys/std/src/m.Signal/u.listeners.ts new file mode 100644 index 0000000000..2e2ca62282 --- /dev/null +++ b/code/sys/std/src/m.Signal/u.listeners.ts @@ -0,0 +1,38 @@ +import { effect as preactEffect } from '@preact/signals-core'; +import { type t, Dispose } from './common.ts'; + +export const listeners: t.SignalLib['listeners'] = (until$) => { + const life = Dispose.lifecycle(until$); + const disposers = new Set<() => void>(); + + const effect = (fn: t.SignalEffectFn) => { + const dispose = preactEffect(fn); + disposers.add(dispose); + + const sub = life.dispose$.subscribe(() => { + dispose(); + disposers.delete(dispose); + sub.unsubscribe(); + }); + + return api; + }; + + life.dispose$.subscribe(() => { + disposers.forEach((dispose) => dispose()); + disposers.clear(); + }); + + /** + * API: + */ + + const api = Dispose.toLifecycle<t.SignalListeners>(life, { + effect, + get count() { + return disposers.size; + }, + }); + + return api; +}; diff --git a/code/sys/std/src/m.Signal/u.toggle.ts b/code/sys/std/src/m.Signal/u.toggle.ts new file mode 100644 index 0000000000..4c92be764d --- /dev/null +++ b/code/sys/std/src/m.Signal/u.toggle.ts @@ -0,0 +1,10 @@ +import { type t } from './common.ts'; + +/** + * Toggle a boolean signal. + */ +export const toggle: t.SignalLib['toggle'] = (signal, forceValue) => { + const next = typeof forceValue === 'boolean' ? forceValue : !signal.value; + signal.value = next; + return next; +}; diff --git a/code/sys/std/src/m.Testing.Server/m.Server.ts b/code/sys/std/src/m.Testing.Server/m.Server.ts index 7f0646ed44..aa19a98983 100644 --- a/code/sys/std/src/m.Testing.Server/m.Server.ts +++ b/code/sys/std/src/m.Testing.Server/m.Server.ts @@ -1,5 +1,6 @@ -import type { t } from '../common/mod.ts'; +import type { t } from '../common.ts'; import { Url } from '../m.Url/mod.ts'; +import { Dispose } from '../m.Dispose/mod.ts'; type M = 'GET' | 'PUT' | 'POST' | 'DELETE'; type H = { method: M; handler: Deno.ServeHandler }; diff --git a/code/sys/std/src/m.Testing.Server/mod.ts b/code/sys/std/src/m.Testing.Server/mod.ts index 8634571532..4ccf4516fa 100644 --- a/code/sys/std/src/m.Testing.Server/mod.ts +++ b/code/sys/std/src/m.Testing.Server/mod.ts @@ -7,7 +7,17 @@ import type { t } from '../common.ts'; import { Testing as Base } from '../m.Testing/mod.ts'; import { TestHttpServer as Http } from './m.HttpServer.ts'; -export { describe, expect, expectError, it } from '../m.Testing/mod.ts'; +export { + afterAll, + afterEach, + Bdd, + beforeAll, + beforeEach, + describe, + expect, + expectError, + it, +} from '../m.Testing/mod.ts'; /** * Testing helpers including light-weight HTTP server helpers (Deno). diff --git a/code/sys/std/src/m.Testing/-Testing.test.ts b/code/sys/std/src/m.Testing/-.test.ts similarity index 56% rename from code/sys/std/src/m.Testing/-Testing.test.ts rename to code/sys/std/src/m.Testing/-.test.ts index cc31e7ed38..7946b036a7 100644 --- a/code/sys/std/src/m.Testing/-Testing.test.ts +++ b/code/sys/std/src/m.Testing/-.test.ts @@ -1,5 +1,5 @@ import { Random } from '../m.Random/mod.ts'; -import { Testing, describe, expect, it, expectError } from './mod.ts'; +import { Time, Testing, describe, expect, it, expectError } from './mod.ts'; Deno.test('Deno.test: sample (down at the test runner metal)', async (test) => { await test.step('eql', () => { @@ -8,10 +8,18 @@ Deno.test('Deno.test: sample (down at the test runner metal)', async (test) => { }); describe('Testing', () => { - it('exports BDD semantics', () => { + it('exports BDD semantics', async () => { + const { describe, it } = await import('@std/testing/bdd'); + const { afterAll, afterEach, beforeAll, beforeEach } = await import('@std/testing/bdd'); + + expect(Testing.Bdd.expect).to.equal(expect); expect(Testing.Bdd.describe).to.equal(describe); expect(Testing.Bdd.it).to.equal(it); - expect(Testing.Bdd.expect).to.equal(expect); + + expect(Testing.Bdd.beforeAll).to.equal(beforeAll); + expect(Testing.Bdd.afterAll).to.equal(afterAll); + expect(Testing.Bdd.beforeEach).to.equal(beforeEach); + expect(Testing.Bdd.afterEach).to.equal(afterEach); }); it('randomPort', () => { @@ -77,4 +85,32 @@ describe('Testing', () => { expect(count).to.eql(3); }); }); + + describe('wait', () => { + it('milliseconds (macro-task queue)', async () => { + const timer = Time.timer(); + await Testing.wait(10); + expect(timer.elapsed.msec).to.be.above(9); + }); + + it('no param (micro-task queue) ā "tick"', async () => { + await Testing.retry(3, async () => { + const timer = Time.timer(); + let microtaskResolved = false; + let macrotaskResolved = false; + + const stop = setTimeout(() => (macrotaskResolved = true), 0); // ā Schedule a macro-task for comparison. + await Testing.wait(); // ā the micro-task delay. + + const elapsed = timer.elapsed.msec; + microtaskResolved = true; + + expect(microtaskResolved).to.be.true; + expect(macrotaskResolved).to.be.false; // Microtasks should run before macrotasks + expect(elapsed).to.eql(0); // Should be ~0ms or very close + + clearTimeout(stop); + }); + }); + }); }); diff --git a/code/sys/std/src/m.Testing/libs.ts b/code/sys/std/src/m.Testing/libs.ts index 80c3b9f62e..f3f7ff0ee6 100644 --- a/code/sys/std/src/m.Testing/libs.ts +++ b/code/sys/std/src/m.Testing/libs.ts @@ -1,2 +1,2 @@ -export { describe, it } from '@std/testing/bdd'; +export { afterAll, afterEach, beforeAll, beforeEach, describe, it } from '@std/testing/bdd'; export { expect } from 'chai'; diff --git a/code/sys/std/src/m.Testing/m.Bdd.ts b/code/sys/std/src/m.Testing/m.Bdd.ts index 7483f4c734..8756c81f08 100644 --- a/code/sys/std/src/m.Testing/m.Bdd.ts +++ b/code/sys/std/src/m.Testing/m.Bdd.ts @@ -1,7 +1,16 @@ -import { describe, expect, it, type t } from './common.ts'; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + type t, +} from './common.ts'; import { expectError } from './u.ts'; -export { describe, expect, it, expectError }; +export { afterAll, afterEach, beforeAll, beforeEach, describe, expect, expectError, it }; /** * BDD semantics ("Behavior Driven Development") helpers. @@ -9,6 +18,12 @@ export { describe, expect, it, expectError }; export const Bdd: t.BddLib = { describe, it, + + beforeAll, + beforeEach, + afterAll, + afterEach, + expect, expectError, }; diff --git a/code/sys/std/src/m.Testing/m.Testing.ts b/code/sys/std/src/m.Testing/m.Testing.ts index 0d0f47f291..d3450cf6d9 100644 --- a/code/sys/std/src/m.Testing/m.Testing.ts +++ b/code/sys/std/src/m.Testing/m.Testing.ts @@ -11,9 +11,10 @@ export const Testing: t.TestingLib = { slug, /** - * Wait for n-milliseconds + * Wait for n-milliseconds. */ wait(msecs): Promise<void> { + if (msecs === undefined) return Promise.resolve(); return new Promise((resolve) => setTimeout(resolve, msecs)); }, diff --git a/code/sys/std/src/m.Testing/mod.ts b/code/sys/std/src/m.Testing/mod.ts index eef702a60d..9f6fd1658c 100644 --- a/code/sys/std/src/m.Testing/mod.ts +++ b/code/sys/std/src/m.Testing/mod.ts @@ -20,12 +20,23 @@ * }); * }); */ -export { Bdd, describe, expect, expectError, it } from './m.Bdd.ts'; +export { + afterAll, + afterEach, + Bdd, + beforeAll, + beforeEach, + describe, + expect, + expectError, + it, +} from './m.Bdd.ts'; + export { Testing } from './m.Testing.ts'; /** * Common utility helpers. */ +export { Time } from '../m.DateTime/mod.ts'; export { Path } from '../m.Path/mod.ts'; export { slug } from '../m.Random/mod.ts'; -export { Time } from '../m.DateTime/mod.ts'; diff --git a/code/sys/std/src/m.Testing/t.ts b/code/sys/std/src/m.Testing/t.ts index 363a0ef60d..1c8bb7e218 100644 --- a/code/sys/std/src/m.Testing/t.ts +++ b/code/sys/std/src/m.Testing/t.ts @@ -1,4 +1,4 @@ -import type { describe, it } from '@std/testing/bdd'; +import type { describe, it, beforeAll, beforeEach, afterAll, afterEach } from '@std/testing/bdd'; import type { expect } from 'chai'; import type { t } from './common.ts'; @@ -9,7 +9,11 @@ export type TestingLib = { readonly FALSY: t.Falsy[]; readonly Bdd: BddLib; slug: t.RandomLib['slug']; - wait(delay: t.Msecs): Promise<void>; + + /** Wait for n-milliseconds, or a "tick" (micrso-task queue) if no delay specified. */ + wait(delay?: t.Msecs): Promise<void>; + + /** Generate a random (unused) port number. */ randomPort(): number; /** Attempt to run the test function <n>-times before throwing. */ @@ -36,12 +40,29 @@ export type Expect = typeof expect; /** Expect an error asyncronously */ export type ExpectError = (fn: () => Promise<any> | any, message?: string) => Promise<any>; +/** Run some shared setup before all of the tests in the group. */ +export type BeforeAll = typeof beforeAll; +/** Run some shared setup before each test in the suite. */ +export type BeforeEach = typeof beforeEach; + +/** Run some shared teardown after all of the tests in the suite. */ +export type AfterAll = typeof afterAll; +/** Run some shared teardown after each test in the suite. */ +export type AfterEach = typeof afterEach; + /** * BDD semantics ("Behavior Driven Development") helpers. */ export type BddLib = { - readonly describe: typeof describe; - readonly it: typeof it; - readonly expect: typeof expect; + readonly describe: Describe; + readonly it: It; + + readonly beforeAll: BeforeAll; + readonly afterAll: AfterAll; + + readonly beforeEach: BeforeEach; + readonly afterEach: AfterEach; + + readonly expect: Expect; readonly expectError: t.ExpectError; }; diff --git a/code/sys/std/src/m.Value.Obj/-.test.ts b/code/sys/std/src/m.Value.Obj/-.test.ts index 08448c83bb..799bd3c7f1 100644 --- a/code/sys/std/src/m.Value.Obj/-.test.ts +++ b/code/sys/std/src/m.Value.Obj/-.test.ts @@ -7,7 +7,7 @@ describe('Value.Obj', () => { expect(Value.Obj).to.equal(Obj); }); - describe('Value.Obj.walk', () => { + describe('Obj.walk', () => { type T = { key: string | number; value: any; path: (string | number)[] }; it('processes object', () => { @@ -141,7 +141,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.build', () => { + describe('Obj.build', () => { it('return default root object (no keyPath)', () => { expect(Value.Obj.build('', {})).to.eql({}); expect(Value.Obj.build(' ', {})).to.eql({}); @@ -213,7 +213,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.pluck', () => { + describe('Obj.pluck', () => { it('returns [undefined] when no match', () => { expect(Value.Obj.pluck('foo', {})).to.eql(undefined); expect(Value.Obj.pluck('foo.bar', {})).to.eql(undefined); @@ -244,7 +244,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.remove', () => { + describe('Obj.remove', () => { const test = (keyPath: string, root: any, expected: any) => { const result = Value.Obj.remove(keyPath, root); const msg = `keyPath: "${keyPath}"`; @@ -281,7 +281,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.prune', () => { + describe('Obj.prune', () => { const test = (keyPath: string, root: any, expected: any) => { const result = Value.Obj.prune(keyPath, root); const msg = `keyPath: "${keyPath}"`; @@ -329,7 +329,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.toArray', () => { + describe('Obj.toArray', () => { type IFoo = { count: number }; type IFoos = { one: IFoo; @@ -364,7 +364,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.trimStringsDeep', () => { + describe('Obj.trimStringsDeep', () => { it('shallow', () => { const name = 'foo'.repeat(100); const obj = { @@ -437,7 +437,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.pick', () => { + describe('Obj.pick', () => { type T = { a: number; b: number; c: number }; const Sample = { create(): T { @@ -475,7 +475,7 @@ describe('Value.Obj', () => { }); }); - describe('Value.Obj.sortKeys', () => { + describe('Obj.sortKeys', () => { it('empty', () => { const obj = {}; const res = Value.Obj.sortKeys(obj); @@ -489,4 +489,167 @@ describe('Value.Obj', () => { expect(Object.keys(res).sort()).to.eql(Object.keys(obj).sort()); }); }); + + describe('Obj.clone', () => { + it('return different instance', () => { + const obj = { foo: 123, bar: { msg: 'hello' } }; + const res = Obj.clone(obj); + expect(res).to.eql(obj); + expect(res).to.not.equal(obj); // NB: different instance. + expect(res.bar).to.not.equal(obj.bar); + }); + + it('circular-reference safe', () => { + type Cycle = { self?: Cycle; msg: string; list: (number | Cycle)[] }; + + const obj: Cycle = { msg: 'š', list: [1] }; + obj.self = obj; + obj.list.push(obj); + obj.list.push(2); + obj.list.push(obj.list[1]); + + const res = Obj.clone(obj); + expect(res).to.eql(obj); + expect(res).to.not.equal(obj); + expect(res.list).to.not.equal(obj.list); + expect(res.list[1]).to.not.equal(obj.list[1]); + }); + + it('should return the same value for primitives', () => { + expect(Obj.clone(null)).to.equal(null); + expect(Obj.clone(undefined)).to.equal(undefined); + expect(Obj.clone(42)).to.equal(42); + expect(Obj.clone('hello')).to.equal('hello'); + expect(Obj.clone(true)).to.equal(true); + }); + + it('should clone arrays properly', () => { + const arr = [1, 2, { a: 3 }]; + const clonedArr = Obj.clone(arr); + expect(clonedArr).to.eql(arr); + expect(clonedArr).to.not.equal(arr); + expect(clonedArr[2]).to.not.equal(arr[2]); + }); + + it('should clone plain objects deeply', () => { + const obj = { a: 1, b: { c: 2 } }; + const clonedObj = Obj.clone(obj); + expect(clonedObj).to.eql(obj); + expect(clonedObj).to.not.equal(obj); + expect(clonedObj.b).to.not.equal(obj.b); + }); + + it('should clone objects with symbol keys', () => { + const sym = Symbol('key'); + const obj = { foo: 'bar', [sym]: 'baz' }; + const clonedObj = Obj.clone(obj); + expect(clonedObj).to.eql(obj); + expect(clonedObj).to.not.equal(obj); + }); + + it('should clone objects with non-enumerable properties', () => { + const obj: any = { visible: 'yes' }; + Object.defineProperty(obj, 'hidden', { + value: 'secret', + enumerable: false, + configurable: true, + writable: true, + }); + const clonedObj = Obj.clone(obj); + expect(clonedObj.visible).to.equal('yes'); + const desc = Object.getOwnPropertyDescriptor(clonedObj, 'hidden'); + expect(desc).to.exist; + expect(desc!.value).to.equal('secret'); + }); + + it('should preserve custom prototypes', () => { + class Custom { + prop: number; + constructor(prop: number) { + this.prop = prop; + } + } + const obj = new Custom(10); + (obj as any).extra = 'test'; + const clonedObj = Obj.clone(obj); + expect(clonedObj).to.eql(obj); + expect(clonedObj).to.not.equal(obj); + expect(Object.getPrototypeOf(clonedObj)).to.equal(Custom.prototype); + }); + + it('should not clone functions, but preserve the same function reference', () => { + const fn = function () { + return 'test'; + }; + const obj = { fn }; + const clonedObj = Obj.clone(obj); + expect(clonedObj.fn).to.equal(fn); + }); + + it('should handle circular references in objects', () => { + type Cycle = { self?: Cycle; msg: string; list: (number | Cycle)[] }; + const obj: Cycle = { msg: 'š', list: [1] }; + obj.self = obj; + obj.list.push(obj); + obj.list.push(2); + obj.list.push(obj.list[1]); // Add an additional cycle: list[1] is the same as obj. + + const cloned = Obj.clone(obj); + expect(cloned).to.eql(obj); + expect(cloned).to.not.equal(obj); + expect(cloned.list).to.not.equal(obj.list); + + // NB: The cloned object's self reference should point to the cloned object. + expect(cloned.self).to.equal(cloned); + + // NB: Verify that cyclic references within the array are maintained. + expect(cloned.list[1]).to.equal(cloned); + }); + + it('should handle circular references in arrays', () => { + const arr: any[] = [1, 2]; + arr.push(arr); + const clonedArr = Obj.clone(arr); + expect(clonedArr).to.eql(arr); + expect(clonedArr).to.not.equal(arr); + + // The cloned array's third element should reference the cloned array itself. + expect(clonedArr[2]).to.equal(clonedArr); + }); + + it('should clone nested objects and arrays', () => { + const obj = { + a: { b: [1, { c: 'hello' }] }, + d: 'world', + }; + const clonedObj = Obj.clone(obj); + expect(clonedObj).to.eql(obj); + expect(clonedObj.a).to.not.equal(obj.a); + expect(clonedObj.a.b).to.not.equal(obj.a.b); + expect(clonedObj.a.b[1]).to.not.equal(obj.a.b[1]); + }); + + it('should clone Date objects (note: date value may not be preserved)', () => { + const date = new Date(); + (date as any).extra = 'data'; + const clonedDate = Obj.clone(date); + expect(clonedDate).to.be.an.instanceof(Date); + + // NB: Extra properties are cloned. + expect((clonedDate as any).extra).to.equal('data'); + expect(clonedDate.getTime()).to.equal(date.getTime()); + }); + + it('should clone RegExp objects (note: pattern and flags may not be preserved)', () => { + const regex = /abc/gi; + (regex as any).extra = 'data'; + const clonedRegex = Obj.clone(regex); + expect(clonedRegex).to.be.an.instanceof(RegExp); + + // NB: The source and flags should match if cloned correctly. + expect(clonedRegex.source).to.equal(regex.source); + expect(clonedRegex.flags).to.equal(regex.flags); + expect((clonedRegex as any).extra).to.equal('data'); + }); + }); }); diff --git a/code/sys/std/src/m.Value.Obj/m.Obj.clone.ts b/code/sys/std/src/m.Value.Obj/m.Obj.clone.ts new file mode 100644 index 0000000000..4b5b4db594 --- /dev/null +++ b/code/sys/std/src/m.Value.Obj/m.Obj.clone.ts @@ -0,0 +1,69 @@ +import { type t } from '../common.ts'; + +/** + * Deeply clone the given object (circular-reference safe) + * with support for Date and RegExp. + */ +export const clone: t.ObjLib['clone'] = (obj) => deepClone(obj); + +/** + * Helpers + */ + +function deepClone<T>(obj: T, visited = new WeakMap()): T { + // Primitives and functions not cloned. + if (obj === null || typeof obj !== 'object') return obj; + + // Return cached clone if available (handle cycles safely). + if (visited.has(obj)) return visited.get(obj); + + // Handle Date objects: + if (obj instanceof Date) { + const clonedDate = new Date(obj.getTime()); + visited.set(obj, clonedDate); + // Clone any extra properties on the Date: + for (const key of Reflect.ownKeys(obj)) { + // NB: Optionally skip built-in keys, though most Date objects won't have extra ones. + if (typeof key === 'string' && ['getTime', 'toString', 'valueOf', 'toJSON'].includes(key)) { + continue; + } + (clonedDate as any)[key] = deepClone((obj as any)[key], visited); // ā š³ RECURSION + } + return clonedDate as unknown as T; + } + + // Handle RegExp objects: + if (obj instanceof RegExp) { + const clonedRegExp = new RegExp(obj.source, obj.flags); + visited.set(obj, clonedRegExp); + + // Clone any extra properties on the RegExp: + for (const key of Reflect.ownKeys(obj)) { + (clonedRegExp as any)[key] = deepClone((obj as any)[key], visited); // ā š³ RECURSION + } + return clonedRegExp as unknown as T; + } + + // Handle arrays: + if (Array.isArray(obj)) { + const arrClone: any[] = []; + visited.set(obj, arrClone); + for (const item of obj) { + arrClone.push(deepClone(item, visited)); // ā š³ RECURSION + } + return arrClone as unknown as T; + } + + // Handle plain objects: + const cloneObj = Object.create(Object.getPrototypeOf(obj)); + visited.set(obj, cloneObj); + + for (const key of Reflect.ownKeys(obj)) { + // Ensure proper copying of both string and symbol keys. + // Also, copying non-enumerable properties might be necessary in some cases: + (cloneObj as any)[key] = deepClone((obj as any)[key], visited); // ā š³ RECURSION + } + + // Finish up. + return cloneObj; +} diff --git a/code/sys/std/src/m.Value.Obj/mod.ts b/code/sys/std/src/m.Value.Obj/mod.ts index 80720b582c..90507a7ae8 100644 --- a/code/sys/std/src/m.Value.Obj/mod.ts +++ b/code/sys/std/src/m.Value.Obj/mod.ts @@ -1,6 +1,10 @@ import type { t } from '../common.ts'; + +import { clone } from './m.Obj.clone.ts'; import { build, pluck, prune, remove } from './m.Obj.path.ts'; -import { pick, toArray, trimStringsDeep, walk, sortKeys } from './m.Obj.ts'; +import { pick, sortKeys, toArray, trimStringsDeep, walk } from './m.Obj.ts'; + +export { sortKeys }; export const Obj: t.ObjLib = { walk, @@ -12,4 +16,5 @@ export const Obj: t.ObjLib = { remove, prune, sortKeys, + clone, }; diff --git a/code/sys/std/src/m.Value.Obj/t.ts b/code/sys/std/src/m.Value.Obj/t.ts index 40773f88fb..f5265ce1ba 100644 --- a/code/sys/std/src/m.Value.Obj/t.ts +++ b/code/sys/std/src/m.Value.Obj/t.ts @@ -65,6 +65,12 @@ export type ObjLib = { * Sort the keys of an object. */ sortKeys<T extends O>(obj: T): T; + + /** + * Deeply clone the given object (circular-reference safe) + * with support for Date and RegExp. + */ + clone<T>(obj: T): T; }; /** A callback passed to the object walker function. */ diff --git a/code/sys/std/src/m.Value.Str/-.test.ts b/code/sys/std/src/m.Value.Str/-.test.ts index 57a9cc9ecb..6619c9dd70 100644 --- a/code/sys/std/src/m.Value.Str/-.test.ts +++ b/code/sys/std/src/m.Value.Str/-.test.ts @@ -246,4 +246,111 @@ describe('Str (Text)', () => { test('--camel-Case', '--camel-case'); }); }); + + describe('String.truncate', () => { + it('returns the original text when length is less than max', () => { + expect(Str.truncate('abc', 5)).to.eql('abc'); + }); + + it('returns the original text when length equals max', () => { + expect(Str.truncate('hello', 5)).to.eql('hello'); + }); + + it('truncates the text when length is greater than max', () => { + // For max = 5, it takes the first 4 characters and appends an ellipsis. + expect(Str.truncate('abcdef', 5)).to.eql('abcdā¦'); + }); + + it('handles max = 1 correctly', () => { + // For a string longer than 1, it returns just the ellipsis. + expect(Str.truncate('abc', 1)).to.eql('ā¦'); + // When the text length equals max, no truncation happens. + expect(Str.truncate('a', 1)).to.eql('a'); + }); + + it('returns an empty string when given an empty string', () => { + expect(Str.truncate('', 3)).to.eql(''); + }); + + it('handles edge case when max is 0', () => { + // With max = 0, "abc" becomes "abc".slice(0, -1) which is "ab", then an ellipsis is appended. + expect(Str.truncate('abc', 0)).to.eql('abā¦'); + }); + + it('handles undefined', () => { + expect(Str.truncate(undefined, 5)).to.eql(''); + expect(Str.truncate(undefined, 0)).to.eql(''); + }); + }); + + describe('Str.Lorem', () => { + const LOREM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque nec quam lorem. Praesent fermentum, augue ut porta varius, eros nisl euismod ante, ac suscipit elit libero nec dolor. Morbi magna enim, molestie non arcu id, varius sollicitudin neque. In sed quam mauris. Aenean mi nisl, elementum non arcu quis, ultrices tincidunt augue. Vivamus fermentum iaculis tellus finibus porttitor. Nulla eu purus id dolor auctor suscipit. Integer lacinia sapien at ante tempus volutpat.`; + const Lorem = Str.Lorem; + + it('Str.lorem (string)', () => { + expect(Str.lorem).to.eql(Str.Lorem.text); + }); + + it('Lorem.text | toString', () => { + expect(Str.Lorem.text).to.eql(LOREM); + expect(Str.Lorem.toString()).to.eql(LOREM); + expect(String(Str.Lorem)).to.eql(LOREM); + }); + + describe('Str.Lorem.words', () => { + it('should return an empty string when count is negative', () => { + const result = Str.Lorem.words(-5); + expect(result).to.equal(''); + }); + + it('should return an empty string when count is zero', () => { + const result = Str.Lorem.words(0); + expect(result).to.equal(''); + }); + + it('should return the first N words with a trailing period when count > 0', () => { + // For example, if count = 5, we expect the result to have 5 words and end with a period. + const count = 5; + const result = Str.Lorem.words(count); + + // Remove the final character (period) to check the word count. + const words = result.split(' '); + expect(words.length).to.equal(count); + expect(result.endsWith('.')).to.be.true; + }); + + it('should repeat the text if count exceeds the total number of words', () => { + const count = 1000; + const result = Str.Lorem.words(count); + + // Remove the trailing period (if present) and split the result into words. + const resultWords = result.endsWith('.') + ? result.slice(0, -1).split(' ') + : result.split(' '); + expect(resultWords.length).to.equal(count); + + // Get the original words from Lorem.text (removing the trailing period). + const originalWords = Str.Lorem.text.endsWith('.') + ? Str.Lorem.text.slice(0, -1).split(' ') + : Str.Lorem.text.split(' '); + + const trimPeriod = (input: string) => input.replace(/\.$/, ''); + + // Verify that the output words repeat the original words in sequence. + for (let i = 0; i < resultWords.length; i++) { + const a = resultWords[i]; + const b = originalWords[i % originalWords.length]; + expect(trimPeriod(a)).to.equal(trimPeriod(b)); + } + + // Ensure the result ends with a period. + expect(result.endsWith('.')).to.be.true; + }); + + it('words - no param', () => { + const result = Str.Lorem.words(); + expect(result.split(' ').length).to.eql(Lorem.text.split(' ').length); + }); + }); + }); }); diff --git a/code/sys/std/src/m.Value.Str/m.Lorem.ts b/code/sys/std/src/m.Value.Str/m.Lorem.ts new file mode 100644 index 0000000000..26afc3fc59 --- /dev/null +++ b/code/sys/std/src/m.Value.Str/m.Lorem.ts @@ -0,0 +1,23 @@ +import type { t } from './common.ts'; + +const LOREM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque nec quam lorem. Praesent fermentum, augue ut porta varius, eros nisl euismod ante, ac suscipit elit libero nec dolor. Morbi magna enim, molestie non arcu id, varius sollicitudin neque. In sed quam mauris. Aenean mi nisl, elementum non arcu quis, ultrices tincidunt augue. Vivamus fermentum iaculis tellus finibus porttitor. Nulla eu purus id dolor auctor suscipit. Integer lacinia sapien at ante tempus volutpat.`; +const total = LOREM.split(' ').length; + +export const Lorem: t.StrLoremLib = { + get text() { + return LOREM; + }, + + toString() { + return LOREM; + }, + + words(count = total) { + if (count < 0) return ''; + const list = LOREM.split(/\s+/).filter((word) => word.length > 0); + const repeats = Math.ceil(count / list.length); + const repeatedWords = Array(repeats).fill(list).flat(); + const result = repeatedWords.slice(0, count).join(' ').trim(); + return result && !result.endsWith('.') ? result + '.' : result; + }, +}; diff --git a/code/sys/std/src/m.Value.Str/m.Str.ts b/code/sys/std/src/m.Value.Str/m.Str.ts index c240b90c6b..6ce5b5ab73 100644 --- a/code/sys/std/src/m.Value.Str/m.Str.ts +++ b/code/sys/std/src/m.Value.Str/m.Str.ts @@ -7,10 +7,14 @@ import { diff } from './u.diff.ts'; import { plural } from './u.plural.ts'; import { shorten } from './u.shorten.ts'; import { replace, splice } from './u.splice.ts'; +import { truncate } from './u.truncate.ts'; +import { Lorem } from './m.Lorem.ts'; export { bytes, capitalize, diff, plural, replace, shorten, splice }; export const Str: t.StrLib = { + Lorem, + lorem: Lorem.text, diff, splice, replace, @@ -19,4 +23,5 @@ export const Str: t.StrLib = { camelToKebab, plural, bytes, + truncate, } as const; diff --git a/code/sys/std/src/m.Value.Str/mod.ts b/code/sys/std/src/m.Value.Str/mod.ts index 314897d00a..e17c5f0d0f 100644 --- a/code/sys/std/src/m.Value.Str/mod.ts +++ b/code/sys/std/src/m.Value.Str/mod.ts @@ -13,4 +13,5 @@ * expect(Value.Str).to.equal(Str); * ``` */ -export { Str, bytes, capitalize, diff, plural, replace, shorten, splice } from './m.Str.ts'; +export { Lorem } from './m.Lorem.ts'; +export { bytes, capitalize, diff, plural, replace, shorten, splice, Str } from './m.Str.ts'; diff --git a/code/sys/std/src/m.Value.Str/t.ts b/code/sys/std/src/m.Value.Str/t.ts index 904f0800ec..379e348bd6 100644 --- a/code/sys/std/src/m.Value.Str/t.ts +++ b/code/sys/std/src/m.Value.Str/t.ts @@ -13,6 +13,11 @@ export type FormatBytesOptions = FormatOptions & {}; * Tools for working on strings of text. */ export type StrLib = { + /** The "lorem ipsum" helper library. */ + readonly Lorem: StrLoremLib; + /** The "lorem ipsum" string. */ + readonly lorem: string; + /** * Calculate a difference between two strings. */ @@ -52,4 +57,18 @@ export type StrLib = { * Convert bytes to a human-readable string, eg: 1337 ā "1.34 kB". */ bytes: t.FormatBytes; + + /** + * Truncates a string with ellipsis if over a maximum length. + */ + truncate(text: string | undefined, max: number): string; +}; + +/** + * Tools for working with sample "lorem ipsum..." text. + */ +export type StrLoremLib = { + readonly text: string; + toString(): string; + words(count?: number): string; }; diff --git a/code/sys/std/src/m.Value.Str/u.truncate.ts b/code/sys/std/src/m.Value.Str/u.truncate.ts new file mode 100644 index 0000000000..274133fb91 --- /dev/null +++ b/code/sys/std/src/m.Value.Str/u.truncate.ts @@ -0,0 +1,5 @@ +import { type t } from './common.ts'; + +export const truncate: t.StrLib['truncate'] = (text = '', max) => { + return text.length > max ? `${text.slice(0, max - 1)}ā¦` : text; +}; diff --git a/code/sys/std/src/m.Value/m.Value.ts b/code/sys/std/src/m.Value/m.Value.ts index 3b97767ac9..2b5cf8fd71 100644 --- a/code/sys/std/src/m.Value/m.Value.ts +++ b/code/sys/std/src/m.Value/m.Value.ts @@ -3,10 +3,10 @@ import { type t, isEmptyRecord, isObject, isRecord } from '../common.ts'; import { Array } from '../m.Value.Array/mod.ts'; import { Num } from '../m.Value.Num/mod.ts'; import { Obj } from '../m.Value.Obj/mod.ts'; -import { Str } from '../m.Value.Str/mod.ts'; +import { Lorem, Str } from '../m.Value.Str/mod.ts'; import { toggle } from './u.toggle.ts'; -export { Array, isEmptyRecord, Num, Obj, Str }; +export { Array, isEmptyRecord, Lorem, Num, Obj, Str }; /** * Tools for evaluating and manipulating types of values. diff --git a/code/sys/std/src/m.Value/mod.ts b/code/sys/std/src/m.Value/mod.ts index a988cb3ef0..981bf8926f 100644 --- a/code/sys/std/src/m.Value/mod.ts +++ b/code/sys/std/src/m.Value/mod.ts @@ -5,5 +5,5 @@ export { isEmptyRecord, isObject, isRecord } from '../common.ts'; export { V } from '../m.Validation/mod.ts'; export { asArray } from '../m.Value.Array/mod.ts'; -export { sortKeys } from '../m.Value.Obj/m.Obj.ts'; -export { Array, Num, Str, Value } from './m.Value.ts'; +export { Obj, sortKeys } from '../m.Value.Obj/mod.ts'; +export { Array, Num, Str, Value, Lorem } from './m.Value.ts'; diff --git a/code/sys/std/src/mod.ts b/code/sys/std/src/mod.ts index 773e19874f..3acfb78738 100644 --- a/code/sys/std/src/mod.ts +++ b/code/sys/std/src/mod.ts @@ -8,7 +8,7 @@ export type * as t from './types.ts'; export { Args } from './m.Args/mod.ts'; export { Async } from './m.Async/mod.ts'; -export { D, Date, Duration, Time } from './m.DateTime/mod.ts'; +export { D, Date, Duration, Time, Timestamp } from './m.DateTime/mod.ts'; export { Delete } from './m.Delete/mod.ts'; export { Dispose } from './m.Dispose/mod.ts'; export { Err } from './m.Err/mod.ts'; @@ -22,7 +22,8 @@ export { maybeWait, Promise } from './m.Promise/mod.ts'; export { slug } from './m.Random/mod.ts'; export { Regex } from './m.Regex/mod.ts'; export { Rx, rx } from './m.Rx/mod.ts'; +export { Signal } from './m.Signal/mod.ts'; export { Url } from './m.Url/mod.ts'; -export { Array, asArray, isObject, isRecord, Num, Str, V, Value } from './m.Value/mod.ts'; +export { Array, asArray, isObject, isRecord, Num, Obj, Str, V, Value } from './m.Value/mod.ts'; export { R } from './common.ts'; diff --git a/code/sys/std/src/pkg.ts b/code/sys/std/src/pkg.ts index 9e70de2172..076d4a2016 100644 --- a/code/sys/std/src/pkg.ts +++ b/code/sys/std/src/pkg.ts @@ -1,8 +1,16 @@ -import type { Pkg as TPkg } from '@sys/types'; -import { default as deno } from '../deno.json' with { type: 'json' }; -import { Pkg } from './m.Pkg/mod.ts'; +import type { Pkg } from '@sys/types'; /** * Package meta-data. -*/ -export const pkg: TPkg = Pkg.fromJson(deno) + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). + */ +export const pkg: Pkg = { name: '@sys/std', version: '0.0.141' }; diff --git a/code/sys/std/src/types.ts b/code/sys/std/src/types.ts index 16c798b457..310a0a9ace 100644 --- a/code/sys/std/src/types.ts +++ b/code/sys/std/src/types.ts @@ -24,6 +24,7 @@ export type * from './m.Regex/t.ts'; export type * from './m.Rx/t.ts'; export type * from './m.Semver.Server/t.ts'; export type * from './m.Semver/t.ts'; +export type * from './m.Signal/t.ts'; export type * from './m.Testing.Server/t.ts'; export type * from './m.Testing/t.ts'; export type * from './m.Url/t.ts'; diff --git a/code/sys/sys/deno.json b/code/sys/sys/deno.json index b215cb9fee..34ecc7df62 100644 --- a/code/sys/sys/deno.json +++ b/code/sys/sys/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/sys", - "version": "0.0.55", + "version": "0.0.65", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/sys/src/pkg.ts b/code/sys/sys/src/pkg.ts index 79cb3f9c80..2aa6d36ded 100644 --- a/code/sys/sys/src/pkg.ts +++ b/code/sys/sys/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/sys', version: '0.0.65' }; diff --git a/code/sys/testing/deno.json b/code/sys/testing/deno.json index d4935bf1e7..e85545d466 100644 --- a/code/sys/testing/deno.json +++ b/code/sys/testing/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/testing", - "version": "0.0.75", + "version": "0.0.85", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys/testing/src/ns.server/common.ts b/code/sys/testing/src/ns.server/common.ts index cc7ffd504c..8c0e74c00d 100644 --- a/code/sys/testing/src/ns.server/common.ts +++ b/code/sys/testing/src/ns.server/common.ts @@ -2,4 +2,16 @@ export * from '../common.ts'; export { c } from '@sys/color/ansi'; export { Fs, Path } from '@sys/fs'; -export { describe, expect, expectError, it, slug, Time } from '@sys/std/testing'; +export { + afterAll, + afterEach, + Bdd, + beforeAll, + beforeEach, + describe, + expect, + expectError, + it, + slug, + Time, +} from '@sys/std/testing'; diff --git a/code/sys/testing/src/ns.server/m.DomMock/u.polyfill.ts b/code/sys/testing/src/ns.server/m.DomMock/u.polyfill.ts index 6883cb5857..00d1ed38e1 100644 --- a/code/sys/testing/src/ns.server/m.DomMock/u.polyfill.ts +++ b/code/sys/testing/src/ns.server/m.DomMock/u.polyfill.ts @@ -1,7 +1,5 @@ import { Window } from 'happy-dom'; - import type { t } from '../common.ts'; -import { Keyboard } from './m.Keyboard.ts'; const g = globalThis as any; let _window: Window | undefined; diff --git a/code/sys/testing/src/pkg.ts b/code/sys/testing/src/pkg.ts index 79cb3f9c80..20e73e2f50 100644 --- a/code/sys/testing/src/pkg.ts +++ b/code/sys/testing/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/testing', version: '0.0.85' }; diff --git a/code/sys/text/deno.json b/code/sys/text/deno.json index ad594c8633..e753eeb062 100644 --- a/code/sys/text/deno.json +++ b/code/sys/text/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/text", - "version": "0.0.73", + "version": "0.0.83", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys/text/src/pkg.ts b/code/sys/text/src/pkg.ts index 79cb3f9c80..ebf828bb64 100644 --- a/code/sys/text/src/pkg.ts +++ b/code/sys/text/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/text', version: '0.0.83' }; diff --git a/code/sys/tmpl/deno.json b/code/sys/tmpl/deno.json index 4f4f84f26c..bacb9e914b 100644 --- a/code/sys/tmpl/deno.json +++ b/code/sys/tmpl/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/tmpl", - "version": "0.0.79", + "version": "0.0.90", "license": "MIT", "tasks": { "lint": "deno lint", diff --git a/code/sys/tmpl/src/-test/mod.ts b/code/sys/tmpl/src/-test/mod.ts index bf35f0f3b4..d498f63fbe 100644 --- a/code/sys/tmpl/src/-test/mod.ts +++ b/code/sys/tmpl/src/-test/mod.ts @@ -1,3 +1,3 @@ -export { describe, expect, it, Testing } from '@sys/testing/server'; +export { describe, expect, expectError, it, Testing } from '@sys/testing/server'; export * from '../common.ts'; export * from './u.SAMPLE.ts'; diff --git a/code/sys/tmpl/src/-test/-sample/.gitignore b/code/sys/tmpl/src/-test/sample-1/.gitignore similarity index 100% rename from code/sys/tmpl/src/-test/-sample/.gitignore rename to code/sys/tmpl/src/-test/sample-1/.gitignore diff --git a/code/sys/tmpl/src/-test/-sample/deno.json b/code/sys/tmpl/src/-test/sample-1/deno.json similarity index 100% rename from code/sys/tmpl/src/-test/-sample/deno.json rename to code/sys/tmpl/src/-test/sample-1/deno.json diff --git a/code/sys/tmpl/src/-test/-sample/docs/index.md b/code/sys/tmpl/src/-test/sample-1/docs/index.md similarity index 100% rename from code/sys/tmpl/src/-test/-sample/docs/index.md rename to code/sys/tmpl/src/-test/sample-1/docs/index.md diff --git a/code/sys/tmpl/src/-test/-sample/mod.ts b/code/sys/tmpl/src/-test/sample-1/mod.ts similarity index 100% rename from code/sys/tmpl/src/-test/-sample/mod.ts rename to code/sys/tmpl/src/-test/sample-1/mod.ts diff --git a/code/sys/tmpl/src/-test/sample-2/README.md b/code/sys/tmpl/src/-test/sample-2/README.md new file mode 100644 index 0000000000..adb251ade3 --- /dev/null +++ b/code/sys/tmpl/src/-test/sample-2/README.md @@ -0,0 +1 @@ +# Sample Readme diff --git a/code/sys/tmpl/src/-test/sample-2/images/volcano.jpg b/code/sys/tmpl/src/-test/sample-2/images/volcano.jpg new file mode 100644 index 0000000000..ab0cef116f Binary files /dev/null and b/code/sys/tmpl/src/-test/sample-2/images/volcano.jpg differ diff --git a/code/sys/tmpl/src/-test/u.SAMPLE.ts b/code/sys/tmpl/src/-test/u.SAMPLE.ts index 636c3b2f7c..b829e76315 100644 --- a/code/sys/tmpl/src/-test/u.SAMPLE.ts +++ b/code/sys/tmpl/src/-test/u.SAMPLE.ts @@ -1,21 +1,39 @@ import { type t, Fs, slug } from '../common.ts'; +type Options = { slug?: boolean }; + +/** + * Test folder/file samples. + */ export const SAMPLE = { - init(options: { source?: t.StringDir; slug?: boolean } = {}) { - const source = Fs.resolve(options.source ?? './src/-test/-sample'); - const target = Fs.resolve(`./.tmp/test/m.Tmpl`, options.slug ?? true ? slug() : ''); - const exists = (dir: t.StringDir, path: string[]) => Fs.exists(Fs.join(dir, ...path)); + fs(namespace: string) { + const target = Fs.join('./.tmp', namespace); return { - source, - target, - ls: { - source: () => Fs.ls(source), - target: () => Fs.ls(target), - }, - exists: { - source: (...path: string[]) => exists(source, path), - target: (...path: string[]) => exists(target, path), - }, + sample1: (options?: Options) => init('./src/-test/sample-1', target, options), + sample2: (options?: Options) => init('./src/-test/sample-2', target, options), } as const; }, } as const; + +/** + * Initialize a test folder. + */ +function init(source: t.StringDir, target: t.StringDir, options: Options = {}) { + const randomDir = options.slug ?? true ? slug() : ''; + source = Fs.resolve(source); + target = Fs.resolve(target, randomDir); + + const exists = (dir: t.StringDir, path: t.StringPath[]) => Fs.exists(Fs.join(dir, ...path)); + return { + source, + target, + ls: { + source: () => Fs.ls(source), + target: () => Fs.ls(target), + }, + exists: { + source: (...path: t.StringPath[]) => exists(source, path), + target: (...path: t.StringPath[]) => exists(target, path), + }, + } as const; +} diff --git a/code/sys/tmpl/src/common/libs.ts b/code/sys/tmpl/src/common/libs.ts index 5adf8e2704..c831ae83ae 100644 --- a/code/sys/tmpl/src/common/libs.ts +++ b/code/sys/tmpl/src/common/libs.ts @@ -1,3 +1,3 @@ -export { Err, Is, rx, slug, Time } from '@sys/std'; export { c, Cli, stripAnsi } from '@sys/cli'; export { Fs, Path } from '@sys/fs'; +export { Err, Is, isRecord, R, rx, slug, Time } from '@sys/std'; diff --git a/code/sys/tmpl/src/m.Log/-.test.ts b/code/sys/tmpl/src/m.Log/-.test.ts index 2961dab25e..cc7cbb4ad0 100644 --- a/code/sys/tmpl/src/m.Log/-.test.ts +++ b/code/sys/tmpl/src/m.Log/-.test.ts @@ -3,22 +3,24 @@ import { Tmpl } from '../m.Tmpl/mod.ts'; import { Log } from './mod.ts'; describe('Tmpl.Log', () => { + const Test = SAMPLE.fs('m.Log'); + it('API', () => { expect(Tmpl.Log).to.equal(Log); }); describe('Log.table', () => { it('log table', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); let change = false; const tmpl = Tmpl.create(test.source, (e) => { if (change) e.modify('// foo'); }); - const res1 = await tmpl.copy(test.target); - const res2 = await tmpl.copy(test.target); + const res1 = await tmpl.write(test.target); + const res2 = await tmpl.write(test.target); change = true; - const res3 = await tmpl.copy(test.target); + const res3 = await tmpl.write(test.target); const table1 = Log.table(res1.ops); const table2 = Log.table(res2.ops); @@ -38,17 +40,17 @@ describe('Tmpl.Log', () => { }); it('empty (no operations)', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source).filter(() => false); - const res = await tmpl.copy(test.target); + const res = await tmpl.write(test.target); const table = Log.table(res.ops); expect(table).to.include('No items to display'); }); it('option: { trimBase:<path> }', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source, (e) => {}); - const res = await tmpl.copy(test.target); + const res = await tmpl.write(test.target); const trimPathLeft = Path.trimCwd(test.target) + '/'; const table = Log.table(res.ops, { trimPathLeft }); @@ -56,9 +58,9 @@ describe('Tmpl.Log', () => { }); it('option: { note: Ęn }', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source, (e) => {}); - const res = await tmpl.copy(test.target); + const res = await tmpl.write(test.target); const table = Log.table(res.ops, { note(op) { diff --git a/code/sys/tmpl/src/m.Tmpl/-.test.ts b/code/sys/tmpl/src/m.Tmpl/-.test.ts index 75c7486190..79fe01ae3c 100644 --- a/code/sys/tmpl/src/m.Tmpl/-.test.ts +++ b/code/sys/tmpl/src/m.Tmpl/-.test.ts @@ -1,11 +1,12 @@ -import { describe, expect, Fs, it, SAMPLE, Time, type t } from '../-test.ts'; +import { type t, describe, expect, expectError, Fs, it, R, SAMPLE, Time } from '../-test.ts'; import { Tmpl } from './mod.ts'; describe('Tmpl', () => { const readFile = async (path: string) => (await Fs.readText(path)).data; + const Test = SAMPLE.fs('m.Tmpl'); const logOps = ( - res: t.TmplCopyResponse, + res: t.TmplWriteResponse, title: string, options: { indent?: number; hideExcluded?: boolean } = {}, ) => { @@ -15,21 +16,21 @@ describe('Tmpl', () => { }; it('init: paths', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source); expect(tmpl.source.absolute).to.eql(test.source); expect(await tmpl.source.ls()).to.eql(await test.ls.source()); tmpl; }); - describe('tmpl.copy:', () => { + describe('tmpl.write:', () => { it('copies all source files', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source); expect(await test.ls.target()).to.eql([]); - const a = await tmpl.copy(test.target); - const b = await tmpl.copy(test.target); + const a = await tmpl.write(test.target); + const b = await tmpl.write(test.target); expect(a.source.absolute).to.eql(test.source); expect(await a.target.ls()).to.eql(await test.ls.target()); @@ -46,21 +47,64 @@ describe('Tmpl', () => { logOps(a, 'Copy:', { indent: 2 }); }); - it('tmpl.copy(): ā create ā update', async () => { - const test = SAMPLE.init(); + it('copies binary files (eg ".jpg")', async () => { + const test = Test.sample2(); + const tmpl = Tmpl.create(test.source); + + const res = await tmpl.write(test.target); + const sourcePath = Fs.join(res.source.absolute, 'images/volcano.jpg'); + const targetPath = Fs.join(res.target.absolute, 'images/volcano.jpg'); + + const source = await Fs.read(sourcePath); + const target = await Fs.read(targetPath); + + expect(String(source.data).startsWith('255,216,255,224,0,16')).to.be.true; // NB: source file contains data. + expect(target.exists).to.eql(true); + expect(target.data).to.eql(source.data); + }); + + it('writes when file-processor function is NOT specified', async () => { + const sample1 = Test.sample1(); + const sample2 = Test.sample2(); + const tmpl1 = Tmpl.create(sample1.source); + const tmpl2 = Tmpl.create(sample2.source); + + await tmpl1.write(sample1.target); + await tmpl2.write(sample2.target); + + const assertIncludes = (paths: string[], path: string) => { + const match = paths.some((p) => p.endsWith(path)); + expect(match).to.be.true; + }; + + const targets1 = await sample1.ls.target(); + const targets2 = await sample2.ls.target(); + + assertIncludes(targets1, '.gitignore'); + assertIncludes(targets1, 'deno.json'); + assertIncludes(targets1, 'mod.ts'); + assertIncludes(targets1, 'docs/index.md'); + + assertIncludes(targets2, 'README.md'); + assertIncludes(targets2, 'images/volcano.jpg'); + }); + + it('tmpl.write(): ā create ā update', async () => { + const test = Test.sample1(); let foo = 0; let count = 0; const tmpl = Tmpl.create(test.source, (e) => { if (e.target.file.name !== 'mod.ts') return e.exclude(); e.modify(`const foo = ${foo}`); expect(e.target.exists).to.eql(count === 0 ? false : true); + expect(e.ctx).to.eql(undefined); count++; }); - const resA = await tmpl.copy(test.target); - foo = 123; // NB: cuase change in file. - const resB = await tmpl.copy(test.target); // NB: "updated" via change flag. - const resC = await tmpl.copy(test.target); // NB: no changes. + const resA = await tmpl.write(test.target); + foo = 123; // NB: cause change in file. + const resB = await tmpl.write(test.target); // NB: "updated" via change flag. + const resC = await tmpl.write(test.target); // NB: no changes. const a = resA.ops.filter((e) => e.written); const b = resB.ops.filter((e) => e.written); @@ -85,14 +129,14 @@ describe('Tmpl', () => { describe('fn: processFile (callback)', () => { it('fn: exclude', async () => { - const { source, target } = SAMPLE.init(); + const { source, target } = Test.sample1(); const tmpl = Tmpl.create(source, async (e) => { await Time.wait(0); // NB: ensure the async variant of the function waits for completion. if (e.target.file.name.endsWith('.md')) e.exclude('user-space'); if (e.target.file.name === '.gitignore') e.exclude(); }); - const res = await tmpl.copy(target); + const res = await tmpl.write(target); for (const op of res.ops) { if (op.file.target.file.name.endsWith('.md')) { @@ -110,23 +154,23 @@ describe('Tmpl', () => { }); it('fn: file source/target', async () => { - const { source, target } = SAMPLE.init(); + const { source, target } = Test.sample1(); let count = 0; const tmpl = Tmpl.create(source, (e) => { count++; expect(e.tmpl.base).to.eql(source); expect(e.target.base).to.eql(target); }); - await tmpl.copy(target); + await tmpl.write(target); expect(count).to.greaterThan(0); // NB: ensure the callback ran. }); it('fn: rename file', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source, (e) => { if (e.target.file.name === 'mod.ts') e.rename('main.ts'); }); - const res = await tmpl.copy(test.target); + const res = await tmpl.write(test.target); const match = res.ops.find((m) => m.file.target.file.name === 'main.ts'); expect(match?.file.tmpl.file.name).to.eql('mod.ts'); expect(match?.file.target.file.name).to.eql('main.ts'); @@ -135,79 +179,198 @@ describe('Tmpl', () => { }); it('fn: modify (file text)', async () => { - const { source, target } = SAMPLE.init(); + const { source, target } = Test.sample1(); const tmpl = Tmpl.create(source, (e) => { + if (!e.text) return; if (e.target.file.name === 'mod.ts') { const next = e.text.tmpl.replace(/\{FOO_BAR\}/g, 'š Hello'); e.modify(next); } }); - const a = await tmpl.copy(target); - const b = await tmpl.copy(target); + const a = await tmpl.write(target); + const b = await tmpl.write(target); const matchA = a.ops.find((m) => m.file.target.file.name === 'mod.ts'); const matchB = b.ops.find((m) => m.file.target.file.name === 'mod.ts'); - expect(matchA?.text.tmpl).to.include(`name: '{FOO_BAR}'`); - expect(matchA?.text.target.before).to.include(''); // NB: Nothing has been written yet. - expect(matchA?.text.target.after).to.include(`name: 'š Hello'`); - expect(matchB?.text.target.before).to.include(`name: 'š Hello'`); // NB: prior written modification (already exists). - expect(await readFile(matchA?.file.target.absolute ?? '')).to.include(`name: 'š Hello'`); + expect(!!(matchA?.text && matchB?.text)).to.be.true; + + if (matchA?.contentType === 'text' && matchB?.contentType === 'text') { + expect(matchA.text.tmpl).to.include(`name: '{FOO_BAR}'`); + expect(matchA.text.target.before).to.include(''); // NB: Nothing has been written yet. + expect(matchA.text.target.after).to.include(`name: 'š Hello'`); + expect(matchA.text.target.isDiff).to.eql(true); + expect(matchB.text.target.before).to.include(`name: 'š Hello'`); // NB: prior written modification (already exists). + expect(await readFile(matchA.file.target.absolute ?? '')).to.include(`name: 'š Hello'`); + } const writtenA = await readFile(a.target.join('mod.ts')); - const writtenB = await readFile(a.target.join('mod.ts')); + const writtenB = await readFile(b.target.join('mod.ts')); expect(writtenA).to.include(`name: 'š Hello'`); expect(writtenB).to.include(`name: 'š Hello'`); }); + + it('fn: modify (binary file)', async () => { + const { source, target } = Test.sample2(); + const jpg = (await Fs.read('./src/-test/sample-2/images/volcano.jpg')).data; + let replaceWith: Uint8Array | undefined; + + const tmpl = Tmpl.create(source, (e) => { + if (e.contentType !== 'binary') return; + if (replaceWith) e.modify(replaceWith); + }); + + const a = await tmpl.write(target); + replaceWith = new Uint8Array([1, 2, 3]); + const b = await tmpl.write(target); + + const matchA = a.ops.find((m) => m.file.target.file.name === 'volcano.jpg'); + const matchB = b.ops.find((m) => m.file.target.file.name === 'volcano.jpg'); + + if (matchA?.contentType === 'binary' && matchB?.contentType === 'binary') { + expect(matchA.binary.tmpl).to.eql(jpg); + expect(matchA.binary.target.before).to.eql(new Uint8Array(0)); // NB: Nothing has been written yet. + expect(matchA.binary.target.after).to.eql(jpg); + expect(matchA.binary.target.isDiff).to.eql(true); + expect(matchB.binary.target.after).to.eql(replaceWith); + } + + const writtenFile = (await Fs.read(b.target.join('images/volcano.jpg'))).data; + expect(writtenFile).to.eql(replaceWith); + }); + + it('fn: modify with wrong type (throws)', async () => { + const sample1 = Test.sample1(); + const sample2 = Test.sample2(); + + const a = Tmpl.create(sample1.source, (e) => { + if (e.contentType === 'text') e.modify(new Uint8Array([1, 2, 3])); // NB: error. + }); + const b = Tmpl.create(sample2.source, (e) => { + if (e.contentType === 'binary') e.modify('fail'); // NB: error. + }); + + await expectError( + () => a.write(sample1.target), + 'Expected string content to update text-file', + ); + + await expectError( + () => b.write(sample2.target), + 'Expected Uint8Array content to update binary-file', + ); + }); + + it('fn: {ctx} passed constructor param', async () => { + const { source, target } = Test.sample1(); + const ctx = { foo: 'root' }; + const fired = [] as any[]; + const tmpl = Tmpl.create(source, { ctx, processFile: (e) => fired.push(e.ctx) }); + + const a = await tmpl.write(target); + const b = await tmpl.write(target, { ctx: { bar: 456 } }); + const c = await tmpl.write(target, { ctx: { foo: 123, bar: 456 } }); + + expect(fired.every((m) => !!m)).to.be.true; + expect(a.ctx).to.eql(ctx); + expect(b.ctx).to.eql({ foo: 'root', bar: 456 }); + expect(c.ctx).to.eql({ foo: 123, bar: 456 }); // NB: root {ctx} overwritten. + }); + + it('fn: {ctx} passed via .write() param', async () => { + const { source, target } = Test.sample1(); + const ctx = { foo: 123 }; + const fired = [] as any[]; + const tmpl = Tmpl.create(source, (e) => fired.push(e.ctx)); + + const a = await tmpl.write(target, { ctx }); + expect(fired.filter(Boolean).length).to.be.greaterThan(1); + expect(fired.every((m) => R.equals(m, ctx))).to.be.true; + expect(a.ctx).to.eql(ctx); + + // No context provided. + const beforeLength = fired.filter(Boolean).length; + const b = await tmpl.write(target); + expect(fired.filter(Boolean).length).to.eql(beforeLength); + expect(b.ctx).to.eql(undefined); + }); }); - describe('fn: beforeCopy | afterCopy (callbacks)', () => { - it('beforeCopy: sync/async', async () => { - const { source, target } = SAMPLE.init(); - const fired: t.TmplCopyHandlerArgs[] = []; + describe('fn: beforeWrite | afterWrite (callbacks)', () => { + it('beforeWrite: sync/async', async () => { + const { source, target } = Test.sample1(); + const fired: t.TmplWriteHandlerArgs[] = []; - const a: t.TmplCopyHandler = (e) => fired.push(e); - const b: t.TmplCopyHandler = async (e) => { + const a: t.TmplWriteHandler = (e) => fired.push(e); + const b: t.TmplWriteHandler = async (e) => { expect((await fired[0].dir.target.ls()).length).to.greaterThan(0); // NB: files already exist. fired.push(e); }; - const tmpl = Tmpl.create(source, { beforeCopy: a }); + const tmpl = Tmpl.create(source, { beforeWrite: a }).filter(() => true); // NB: .filter tests that the before/after handlers are passed through. - await tmpl.copy(target); + await tmpl.write(target); expect(fired.length).to.eql(1); - await tmpl.copy(target, { beforeCopy: [b] }); + await tmpl.write(target, { onBefore: [b] }); expect(fired.length).to.eql(3); // NB: 2-more (the constructor callback PLUS callback passed to the copy paramemter). + expect(fired.every((m) => m.ctx === undefined)).to.eql(true); // NB: {ctx} not passed to the [Tmpl.write] method. }); - it('afterCopy: sync/async', async () => { - const { source, target } = SAMPLE.init(); - const fired: t.TmplCopyHandlerArgs[] = []; + it('afterWrite: sync/async', async () => { + const { source, target } = Test.sample1(); + const fired: t.TmplWriteHandlerArgs[] = []; - const a: t.TmplCopyHandler = (e) => fired.push(e); - const b: t.TmplCopyHandler = async (e) => { + const a: t.TmplWriteHandler = (e) => fired.push(e); + const b: t.TmplWriteHandler = async (e) => { expect((await fired[0].dir.target.ls()).length).to.greaterThan(0); // NB: files already exist. fired.push(e); }; - const tmpl = Tmpl.create(source, { afterCopy: a }); + const tmpl = Tmpl.create(source, { afterWrite: a }); - await tmpl.copy(target); + await tmpl.write(target); expect(fired.length).to.eql(1); - await tmpl.copy(target, { afterCopy: [b] }); + await tmpl.write(target, { onAfter: [b] }); expect(fired.length).to.eql(3); // NB: 2-more (the constructor callback PLUS callback passed to the copy paramemter). + expect(fired.every((m) => m.ctx === undefined)).to.eql(true); // NB: {ctx} not passed to the [Tmpl.write] method. + }); + + it('{ctx} passed via .write() param', async () => { + const { source, target } = Test.sample1(); + const ctx = { foo: 123 }; + const fired = { + before: [] as any[], + after: [] as any[], + }; + const onBefore: t.TmplWriteHandler = (e) => fired.before.push(e.ctx); + const onAfter: t.TmplWriteHandler = (e) => fired.after.push(e.ctx); + const tmpl = Tmpl.create(source); + + // No context provided, so {ctx} not passed through before/after callbacks. + await tmpl.write(target, { onBefore, onAfter }); + expect(fired.before.length).to.greaterThan(0); + expect(fired.after.length).to.greaterThan(0); + expect(fired.before.filter(Boolean).length).to.eql(0); + expect(fired.after.filter(Boolean).length).to.eql(0); + + // Before/after callbacks provided with {ctx}. + await tmpl.write(target, { ctx, onBefore, onAfter }); + expect(fired.before.filter(Boolean).length).to.eql(1); + expect(fired.after.filter(Boolean).length).to.eql(1); + expect(fired.before.filter(Boolean).every((m) => R.equals(m, ctx))).to.be.true; + expect(fired.after.filter(Boolean).every((m) => R.equals(m, ctx))).to.be.true; }); }); describe('flag: force', () => { it('force', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source); - const resA = await tmpl.copy(test.target); - const resB = await tmpl.copy(test.target); // NB: no changes (already written). - const resC = await tmpl.copy(test.target, { force: true }); + const resA = await tmpl.write(test.target); + const resB = await tmpl.write(test.target); // NB: no changes (already written). + const resC = await tmpl.write(test.target, { force: true }); expect(resA.ops.every((m) => m.forced === false)).to.be.true; expect(resB.ops.every((m) => m.forced === false)).to.be.true; @@ -224,14 +387,14 @@ describe('Tmpl', () => { }); it('force ā (with exclusions)', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source, (e) => { if (!e.target.exists) return; // NB: create the user-space file if it does not yet exist if (e.target.file.name === 'doc.md') e.exclude('user-space'); }); - const resA = await tmpl.copy(test.target); - const resB = await tmpl.copy(test.target, { force: true }); + const resA = await tmpl.write(test.target); + const resB = await tmpl.write(test.target, { force: true }); expect(await test.exists.target('docs/index.md')).to.eql(true); const indent = 2; @@ -242,11 +405,11 @@ describe('Tmpl', () => { }); }); - describe('flag: write (default: true)', () => { - it('write: false', async () => { - const test = SAMPLE.init(); + describe('flag: dryRun (default: false)', () => { + it('dryRun: true (does not write)', async () => { + const test = Test.sample1(); const tmpl = Tmpl.create(test.source); - const res = await tmpl.copy(test.target, { write: false }); + const res = await tmpl.write(test.target, { dryRun: true }); expect(res.ops.every((m) => m.written === false)).to.be.true; for (const op of res.ops) { expect(await Fs.exists(op.file.target.absolute)).to.eql(false); @@ -254,9 +417,9 @@ describe('Tmpl', () => { }); it('logs as "dry run"', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source); - const res = await tmpl.copy(test.target, { write: false }); + const res = await tmpl.write(test.target, { dryRun: true }); const table = Tmpl.Log.table(res.ops); expect(table).to.include('dry-run'); console.info(table); @@ -270,7 +433,7 @@ describe('Tmpl', () => { }; it('single-level filter', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl1 = Tmpl.create(test.source); const tmpl2 = Tmpl.create(test.source).filter((e) => e.file.name !== '.gitignore'); expect(includes(await tmpl1.source.ls(), '/.gitignore')).to.be.true; @@ -278,7 +441,7 @@ describe('Tmpl', () => { }); it('multi-level filter', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl1 = Tmpl.create(test.source).filter((e) => e.file.name !== '.gitignore'); const tmpl2 = tmpl1.filter((e) => !e.file.name.endsWith('.md')); expect(includes(await tmpl1.source.ls(), '/.gitignore')).to.be.false; @@ -287,9 +450,9 @@ describe('Tmpl', () => { }); it('does not copy filtered files', async () => { - const test = SAMPLE.init(); + const test = Test.sample1(); const tmpl = Tmpl.create(test.source).filter((e) => !e.file.name.endsWith('.md')); - await tmpl.copy(test.target); + await tmpl.write(test.target); const paths = await test.ls.target(); expect(paths.length).to.greaterThan(2); diff --git a/code/sys/tmpl/src/m.Tmpl/t.ts b/code/sys/tmpl/src/m.Tmpl/t.ts index fae8511684..1fc14debe5 100644 --- a/code/sys/tmpl/src/m.Tmpl/t.ts +++ b/code/sys/tmpl/src/m.Tmpl/t.ts @@ -1,5 +1,7 @@ import type { t } from './common.ts'; +type O = Record<string, unknown>; + /** * Library for copying template files. */ @@ -21,9 +23,17 @@ export type TmplFactory = ( /** Options passed to the template engine factory. */ export type TmplFactoryOptions = { + /** Handler to run after the write operation completes. */ + beforeWrite?: t.TmplWriteHandler; + + /** Handler to process each file in the template. */ processFile?: t.TmplProcessFile; - beforeCopy?: t.TmplCopyHandler; - afterCopy?: t.TmplCopyHandler; + + /** Handler to run after the write operation completes. */ + afterWrite?: t.TmplWriteHandler; + + /** Context data passed to the process handler. */ + ctx?: O; }; /** @@ -34,7 +44,7 @@ export type Tmpl = { readonly source: t.FsDir; /** Perform a copy of the templates to a target directory. */ - copy(target: t.StringDir, options?: t.TmplCopyOptions): Promise<t.TmplCopyResponse>; + write(target: t.StringDir, options?: t.TmplWriteOptions): Promise<t.TmplWriteResponse>; /** Clones the template filtering down to a subset of source files. */ filter(fn: t.TmplFilter): t.Tmpl; @@ -58,24 +68,44 @@ export type TmplFilter = t.FsFileFilter; */ export type TmplProcessFile = (args: TmplProcessFileArgs) => TmplProcessFileResponse; export type TmplProcessFileResponse = t.IgnoredResult | Promise<t.IgnoredResult>; -export type TmplProcessFileArgs = { +export type TmplProcessFileArgs = t.TmplProcessTextFileArgs | TmplProcessBinaryFileArgs; + +type FileArgs = { + /** Optional context passed to the `Tmpl.write` operation. */ + readonly ctx?: O; + /** The source template file. */ readonly tmpl: t.FsFile; /** The target location being copied to. */ readonly target: t.FsFile & { exists: boolean }; - /** The text body of the file. */ - readonly text: { tmpl: string; current: string }; - /** Filter out the file from being copied. */ exclude(reason?: string): TmplProcessFileArgs; /** Adjust the name of the file. */ rename(filename: string): TmplProcessFileArgs; - /** Adjust the text within the file. */ - modify(text: string): TmplProcessFileArgs; + /** Adjust the content of the file. */ + modify(next: string | Uint8Array): TmplProcessTextFileArgs; +}; + +/** Arguments passed to a text-file for processing. */ +export type TmplProcessTextFileArgs = FileArgs & { + /** The content-type of the template file. */ + readonly contentType: t.TmplTextFileOperation['contentType']; + /** The text body of the file. */ + readonly text: { tmpl: string; current: string }; + readonly binary: undefined; +}; + +/** Arguments passed to a binary-file for processing. */ +export type TmplProcessBinaryFileArgs = FileArgs & { + /** The content-type of the template file. */ + readonly contentType: t.TmplBinaryFileOperation['contentType']; + /** The text body of the file. */ + readonly binary: { tmpl: Uint8Array; current: Uint8Array }; + readonly text: undefined; }; /** @@ -83,40 +113,46 @@ export type TmplProcessFileArgs = { * Use this to do either clean up, or additional setup actions not handled * directly by the template-copy engine. */ -export type TmplCopyHandler = (e: TmplCopyHandlerArgs) => t.IgnoredResult; -/** Arguments passed to the `afterCopy` callback. */ -export type TmplCopyHandlerArgs = { +export type TmplWriteHandler = (e: TmplWriteHandlerArgs) => t.IgnoredResult; +/** Arguments passed to the write handler. */ +export type TmplWriteHandlerArgs = { readonly dir: { readonly source: t.FsDir; readonly target: t.FsDir }; + readonly ctx?: O; }; /** Options passed to the `tmpl.copy` method. */ -export type TmplCopyOptions = { +export type TmplWriteOptions = { /** Flag indicating if the copy operation should be forced. (NB: "excluded" paths will never be written). */ force?: boolean; - /** Flag indicating if the files should be written. Default: true (pass false for a "dry-run"). */ - write?: boolean; + /** Flag indicating if the files should be written. Default: false. */ + dryRun?: boolean; /** Handler(s) to run before the copy operation starts. */ - beforeCopy?: t.TmplCopyHandler | t.TmplCopyHandler[]; + onBefore?: t.TmplWriteHandler | t.TmplWriteHandler[]; /** Handler(s) to run after the copy operation completes. */ - afterCopy?: t.TmplCopyHandler | t.TmplCopyHandler[]; + onAfter?: t.TmplWriteHandler | t.TmplWriteHandler[]; + + /** Context data passed to the process handler. */ + ctx?: O; }; /** * The reponse returned from the `tmpl.copy` method. */ -export type TmplCopyResponse = { +export type TmplWriteResponse = { readonly source: t.FsDir; readonly target: t.FsDir; readonly ops: t.TmplFileOperation[]; + readonly ctx?: O; }; /** * Details about a file update. */ -export type TmplFileOperation = { +export type TmplFileOperation = TmplTextFileOperation | TmplBinaryFileOperation; +type Operation = { /** If excluded, contains the reason for the exclusion, otherwise `boolean` flag. */ excluded: boolean | { reason: string }; @@ -134,10 +170,42 @@ export type TmplFileOperation = { /** File path details. */ file: { tmpl: t.FsFile; target: t.FsFile }; +}; + +/** The content-type contained within the template file. */ +export type TmplFileContentType = TmplFileOperation['contentType']; + +export type TmplTextFileOperation = Operation & { + /** The content-type of the template file. */ + readonly contentType: 'text'; + readonly binary: undefined; /** The text content of the file. */ - text: { - tmpl: string; - target: { before: string; after: string; isDiff: boolean }; - }; + readonly text: t.TmplFileOperationText; +}; + +export type TmplBinaryFileOperation = Operation & { + /** The content-type of the template file. */ + readonly contentType: 'binary'; + readonly text: undefined; + + /** The binary content of the file. */ + readonly binary: t.TmplFileOperationBinary; +}; + +/** The text content of the file. */ +export type TmplFileOperationText = { + /** The source template before transform. */ + readonly tmpl: string; + /** Details about the template file at the target location. */ + readonly target: { before: string; after: string; isDiff: boolean }; +}; + +/** The binary content of the file. */ +export type TmplFileOperationBinary = { + /** The source template before transform. */ + readonly tmpl: Uint8Array; + + /** Details about the template file at the target location. */ + readonly target: { before: Uint8Array; after: Uint8Array; isDiff: boolean }; }; diff --git a/code/sys/tmpl/src/m.Tmpl/u.copy.ts b/code/sys/tmpl/src/m.Tmpl/u.copy.ts deleted file mode 100644 index cdbce42ccd..0000000000 --- a/code/sys/tmpl/src/m.Tmpl/u.copy.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { type t, Fs } from './common.ts'; - -type Changes = { - excluded: t.TmplFileOperation['excluded']; - filename: string; - text: string; -}; - -export async function copy( - source: t.FsDir, - target: t.FsDir, - fn: t.TmplProcessFile | undefined, - options: t.TmplCopyOptions = {}, -) { - const forced = options.force ?? false; - const ops: t.TmplFileOperation[] = []; - const res: t.TmplCopyResponse = { - get source() { - return source; - }, - get target() { - return target; - }, - get ops() { - return ops; - }, - }; - - const copyArgs: t.TmplCopyHandlerArgs = { - get dir() { - return { source, target }; - }, - }; - - /** - * Run BEFORE handlers. - */ - for (const fn of wrangle.copyHandlers(options.beforeCopy)) { - await fn(copyArgs); - } - - /** - * Perform copy on files. - */ - for (const from of await source.ls()) { - if (await Fs.Is.dir(from)) continue; - - const to = Fs.join(target.absolute, from.slice(source.absolute.length + 1)); - const sourceText = (await Fs.readText(from)).data ?? ''; - const targetText = (await Fs.readText(to)).data ?? ''; - - type T = t.TmplFileOperation; - let _file: T['file']; - let _text: T['text']; - - const op: T = { - get file() { - if (_file) return _file; - return (_file = { - tmpl: Fs.toFile(from, source.absolute), - target: Fs.toFile(to, target.absolute), - }); - }, - get text() { - if (_text) return _text; - return (_text = { - tmpl: sourceText, - target: { - before: targetText, - after: targetText || sourceText, - get isDiff() { - return op.text.target.before !== op.text.target.after; - }, - }, - }); - }, - excluded: false, - written: false, - created: false, - updated: false, - forced, - }; - ops.push(op); - - if (typeof fn === 'function') { - const { args, changes } = await wrangle.args(op); - await fn(args); - if (changes.excluded) op.excluded = changes.excluded; - if (changes.filename) op.file.target = wrangle.rename(op.file.target, changes.filename); - if (changes.text) { - op.text.target.after = changes.text; // Update to modified output. - } else { - op.text.target.after = args.text.tmpl; // Update to current template. - } - } - - if (!op.excluded) { - const target = op.file.target; - const path = target.absolute; - const exists = await Fs.exists(path); - const isDiff = op.text.target.isDiff; - - if (!exists) { - op.created = true; - } else if (isDiff || op.forced) { - op.updated = true; - } - - if ((op.created || op.updated) && (options.write ?? true)) { - op.written = true; - - await Fs.ensureDir(Fs.dirname(path)); - await Deno.writeTextFile(path, op.text.target.after); - } - } - } - - /** - * Run AFTER handlers. - */ - for (const fn of wrangle.copyHandlers(options.afterCopy)) { - await fn(copyArgs); - } - - // Finish up. - return res; -} - -/** - * Helpers - */ -const wrangle = { - async args(op: t.TmplFileOperation) { - const changes: Changes = { excluded: false, filename: '', text: '' }; - const { tmpl, target } = op.file; - const exists = await Fs.exists(target.absolute); - const args: t.TmplProcessFileArgs = { - get tmpl() { - return tmpl; - }, - get target() { - return { ...target, exists }; - }, - get text() { - return { tmpl: op.text.tmpl, current: op.text.target.before }; - }, - exclude(reason) { - changes.excluded = typeof reason === 'string' ? { reason } : true; - return args; - }, - rename(filename) { - changes.filename = filename; - return args; - }, - modify(text) { - changes.text = text; - return args; - }, - }; - return { args, changes } as const; - }, - - rename(input: t.FsFile, newFilename: string): t.FsFile { - return Fs.toFile(Fs.join(input.dir, newFilename), input.base); - }, - - copyHandlers(input?: t.TmplCopyHandler | t.TmplCopyHandler[]): t.TmplCopyHandler[] { - if (!input) return []; - const res = Array.isArray(input) ? input : [input]; - return res.flat(Infinity).filter(Boolean); - }, -} as const; diff --git a/code/sys/tmpl/src/m.Tmpl/u.create.ts b/code/sys/tmpl/src/m.Tmpl/u.create.ts index a6f2106aa8..cd975eb273 100644 --- a/code/sys/tmpl/src/m.Tmpl/u.create.ts +++ b/code/sys/tmpl/src/m.Tmpl/u.create.ts @@ -1,12 +1,14 @@ -import { type t, Fs } from './common.ts'; -import { copy } from './u.copy.ts'; +import { type t, Fs, isRecord } from './common.ts'; +import { write } from './u.write.ts'; + +type O = Record<string, unknown>; /** * Create a new directory template. */ export const create: t.TmplFactory = (sourceDir, opt) => { - const { processFile, beforeCopy, afterCopy } = wrangle.options(opt); - return factory({ sourceDir, beforeCopy, processFile, afterCopy }); + const { processFile, beforeWrite, afterWrite, ctx } = wrangle.options(opt); + return factory({ sourceDir, beforeWrite, processFile, afterWrite, ctx }); }; /** @@ -14,10 +16,11 @@ export const create: t.TmplFactory = (sourceDir, opt) => { */ function factory(args: { sourceDir: t.StringDir; - beforeCopy?: t.TmplCopyHandler; + beforeWrite?: t.TmplWriteHandler; processFile?: t.TmplProcessFile; - afterCopy?: t.TmplCopyHandler; + afterWrite?: t.TmplWriteHandler; filter?: t.FsFileFilter[]; + ctx?: O; }): t.Tmpl { const { sourceDir, processFile } = args; const source = Fs.toDir(sourceDir, args.filter); @@ -25,15 +28,16 @@ function factory(args: { get source() { return source; }, - copy(target, options = {}) { - const beforeCopy = wrangle.copyHandlers(args.beforeCopy, options.beforeCopy); - const afterCopy = wrangle.copyHandlers(args.afterCopy, options.afterCopy); - return copy(source, Fs.toDir(target), processFile, { ...options, beforeCopy, afterCopy }); + write(target, options = {}) { + const onBefore = wrangle.writeHandlers(args.beforeWrite, options.onBefore); + const onAfter = wrangle.writeHandlers(args.afterWrite, options.onAfter); + const ctx = wrangle.ctx(args.ctx, options.ctx); + return write(source, Fs.toDir(target), processFile, { ...options, onBefore, onAfter, ctx }); }, filter(next) { - const { sourceDir, processFile } = args; + const { sourceDir, processFile, beforeWrite, afterWrite } = args; const filter = [...(args.filter ?? []), next]; - return factory({ sourceDir, processFile, filter }); + return factory({ sourceDir, processFile, beforeWrite, afterWrite, filter }); }, }; return tmpl; @@ -49,8 +53,12 @@ const wrangle = { return input; }, - copyHandlers(base?: t.TmplCopyHandler, param?: t.TmplCopyHandler | t.TmplCopyHandler[]) { - type T = t.TmplCopyHandler; + writeHandlers(base?: t.TmplWriteHandler, param?: t.TmplWriteHandler | t.TmplWriteHandler[]) { + type T = t.TmplWriteHandler; return [param, base].flat(Infinity).filter(Boolean) as T[]; }, + + ctx(root?: O, write?: O): O | undefined { + return isRecord(root) || isRecord(write) ? { ...root, ...write } : undefined; + }, } as const; diff --git a/code/sys/tmpl/src/m.Tmpl/u.write.args.ts b/code/sys/tmpl/src/m.Tmpl/u.write.args.ts new file mode 100644 index 0000000000..f1ccc437fb --- /dev/null +++ b/code/sys/tmpl/src/m.Tmpl/u.write.args.ts @@ -0,0 +1,62 @@ +import { type t, Fs, Is } from './common.ts'; + +type O = Record<string, unknown>; + +export type Changes = { + excluded: t.TmplFileOperation['excluded']; + filename?: string; + text?: string; + binary?: Uint8Array; +}; + +export async function createArgs(op: t.TmplFileOperation, ctx?: O) { + const changes: Changes = { excluded: false }; + + const { tmpl, target } = op.file; + const exists = await Fs.exists(target.absolute); + const isText = op.contentType === 'text'; + const isBinary = op.contentType === 'binary'; + + const args: t.TmplProcessFileArgs = { + contentType: op.contentType as any, + get ctx() { + return ctx; + }, + get tmpl() { + return tmpl; + }, + get target() { + return { ...target, exists }; + }, + get text() { + if (!isText) return undefined; + return { tmpl: op.text!.tmpl, current: op.text!.target.before }; + }, + get binary() { + if (!isBinary) return undefined; + const tmpl = op.binary!.tmpl; + const current = op.binary!.target.before; + return { tmpl, current } as any; // NB: type hack. + }, + exclude(reason) { + changes.excluded = typeof reason === 'string' ? { reason } : true; + return args; + }, + rename(filename) { + changes.filename = filename; + return args; + }, + modify(input: any) { + if (isText && typeof input !== 'string') { + throw new Error(`Expected string content to update text-file: ${target.relative}`); + } + if (isBinary && !Is.uint8Array(input)) { + throw new Error(`Expected Uint8Array content to update binary-file: ${target.relative}`); + } + if (isText) changes.text = input; + if (isBinary) changes.binary = input; + return args as any; + }, + }; + return { args, changes } as const; +} diff --git a/code/sys/tmpl/src/m.Tmpl/u.write.ts b/code/sys/tmpl/src/m.Tmpl/u.write.ts new file mode 100644 index 0000000000..fd66cc5e46 --- /dev/null +++ b/code/sys/tmpl/src/m.Tmpl/u.write.ts @@ -0,0 +1,212 @@ +import { type t, Fs } from './common.ts'; +import { createArgs } from './u.write.args.ts'; + +type O = Record<string, unknown>; + +export async function write( + source: t.FsDir, + target: t.FsDir, + fn: t.TmplProcessFile | undefined, + options: t.TmplWriteOptions = {}, +) { + const { ctx, dryRun } = options; + const forced = options.force ?? false; + + const ops: t.TmplFileOperation[] = []; + const res: t.TmplWriteResponse = { + get source() { + return source; + }, + get target() { + return target; + }, + get ops() { + return ops; + }, + get ctx() { + return ctx; + }, + }; + + const copyArgs: t.TmplWriteHandlerArgs = { + ctx, + get dir() { + return { source, target }; + }, + }; + + /** + * Run BEFORE handlers. + */ + for (const fn of wrangle.copyHandlers(options.onBefore)) { + await fn(copyArgs); + } + + /** + * Perform copy on files. + */ + for (const from of await source.ls()) { + if (await Fs.Is.dir(from)) continue; + const isBinary = await Fs.Is.binary(from); + const isText = !isBinary; + const to = Fs.join(target.absolute, from.slice(source.absolute.length + 1)); + + const Lazy = { + file() { + let _prop: t.TmplFileOperation['file']; + return () => { + if (_prop) return _prop; + return (_prop = { + tmpl: Fs.toFile(from, source.absolute), + target: Fs.toFile(to, target.absolute), + }); + }; + }, + async text() { + type R = t.TmplTextFileOperation['text']; + const source = isText ? (await Fs.readText(from)).data ?? '' : ''; + const target = isText ? (await Fs.readText(to)).data ?? '' : ''; + let _prop: R; + return (): R => { + if (_prop) return _prop; + return (_prop = { + tmpl: source, + target: { + before: target, + after: target || source, + get isDiff() { + return _prop?.target.before !== _prop?.target.after; + }, + }, + }); + }; + }, + async binary() { + type R = t.TmplBinaryFileOperation['binary']; + const empty = new Uint8Array(0); + const source = isBinary ? (await Fs.read(from)).data ?? empty : empty; + const target = isBinary ? (await Fs.read(to)).data ?? empty : empty; + let _prop: R; + return (): R => { + if (_prop) return _prop; + return (_prop = { + tmpl: source, + target: { + before: target, + after: target || source, + get isDiff() { + return !arraysEqual(_prop?.target.before, _prop?.target.after); + }, + }, + }); + }; + }, + } as const; + + const fileProp = Lazy.file(); + const textProp = await Lazy.text(); + const binaryProp = await Lazy.binary(); + + const op: t.TmplFileOperation = { + contentType: (isText ? 'text' : 'binary') as any, + get file() { + return fileProp(); + }, + get text() { + return (isText ? textProp() : undefined) as any; // NB: type-hack. + }, + get binary() { + return (isBinary ? binaryProp() : undefined) as any; // NB: type-hack. + }, + excluded: false, + written: false, + created: false, + updated: false, + forced, + }; + ops.push(op); + + /** + * Run "process file" handler. + */ + const { args, changes } = await createArgs(op, ctx); + if (typeof fn === 'function') { + await fn(args); + + if (changes.excluded) op.excluded = changes.excluded; + if (changes.filename) op.file.target = wrangle.rename(op.file.target, changes.filename); + if (isText) { + if (changes.text) { + op.text!.target.after = changes.text; // Update to: modified output. + } else { + op.text!.target.after = args.text!.tmpl; // Update to: current template. + } + } + if (isBinary) { + if (changes.binary) { + op.binary!.target.after = changes.binary; // Update to: modified output. + } else { + op.binary!.target.after = args.binary!.tmpl; // Update to: current template. + } + } + } else { + if (isText) op.text!.target.after = args.text!.tmpl; // Update to: current template. + if (isBinary) op.binary!.target.after = args.binary!.tmpl; // Update to: current template. + } + + if (!op.excluded) { + const target = op.file.target; + const path = target.absolute; + const exists = await Fs.exists(path); + + let isDiff = false; + if (isText) isDiff = op.text!.target.isDiff; + if (isBinary) isDiff = op.binary!.target.isDiff; + + if (!exists) { + op.created = true; + } else if (isDiff || op.forced) { + op.updated = true; + } + + if (!dryRun && (op.created || op.updated)) { + let data: string | Uint8Array | undefined; + if (isText) data = op.text!.target.after; + if (isBinary) data = op.binary!.target.after; + if (data) { + await Fs.write(path, data, { throw: true }); + op.written = true; + } + } + } + } + + /** + * Run AFTER handlers. + */ + for (const fn of wrangle.copyHandlers(options.onAfter)) { + await fn(copyArgs); + } + + // Finish up. + return res; +} + +/** + * Helpers + */ +const wrangle = { + rename(input: t.FsFile, newFilename: string): t.FsFile { + return Fs.toFile(Fs.join(input.dir, newFilename), input.base); + }, + + copyHandlers(input?: t.TmplWriteHandler | t.TmplWriteHandler[]): t.TmplWriteHandler[] { + if (!input) return []; + const res = Array.isArray(input) ? input : [input]; + return res.flat(Infinity).filter(Boolean); + }, +} as const; + +function arraysEqual(a: Uint8Array, b: Uint8Array): boolean { + return a.length === b.length && a.every((value, index) => value === b[index]); +} diff --git a/code/sys/tmpl/src/pkg.ts b/code/sys/tmpl/src/pkg.ts index 79cb3f9c80..d0d1339d3d 100644 --- a/code/sys/tmpl/src/pkg.ts +++ b/code/sys/tmpl/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@sys/tmpl', version: '0.0.90' }; diff --git a/code/sys/types/deno.json b/code/sys/types/deno.json index ac4c591cf1..4c95229dd0 100644 --- a/code/sys/types/deno.json +++ b/code/sys/types/deno.json @@ -1,6 +1,6 @@ { "name": "@sys/types", - "version": "0.0.82", + "version": "0.0.92", "license": "MIT", "tasks": { "test": "deno test -RW", diff --git a/code/sys/types/src/pkg.ts b/code/sys/types/src/pkg.ts index dfc4462424..40a7a990a7 100644 --- a/code/sys/types/src/pkg.ts +++ b/code/sys/types/src/pkg.ts @@ -1,8 +1,16 @@ -import type { t } from '@sys/types'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = {name: deno.name, version: deno.version }; +export const pkg: Pkg = { name: '@sys/types', version: '0.0.92' }; diff --git a/code/sys/types/src/types/t.Dispose.ts b/code/sys/types/src/types/t.Dispose.ts index 397939d9f0..ac8bd027d1 100644 --- a/code/sys/types/src/types/t.Dispose.ts +++ b/code/sys/types/src/types/t.Dispose.ts @@ -13,6 +13,10 @@ export type Disposable = { dispose(): void; }; +/** The "until this fires" input for a disposable resource factory. */ +export type DisposeInput = t.UntilObservable | t.Disposable; +export type UntilInput = DisposeInput; + /** * An object that provides a standard asynchronous destructor pattern. */ @@ -54,7 +58,7 @@ export type LifecycleAsync = DisposableAsync & { readonly disposed: boolean }; /** * Utility Type: remove fields from composite Dispose object. */ -export type OmitDisposable<T extends Disposable | DisposableAsync> = Omit< +export type OmitDisposable<T extends Disposable | DisposableAsync | object> = Omit< T, 'dispose' | 'dispose$' >; @@ -62,7 +66,7 @@ export type OmitDisposable<T extends Disposable | DisposableAsync> = Omit< /** * Utility Type: remove fields from composite Lifecycle object. */ -export type OmitLifecycle<T extends Lifecycle | LifecycleAsync> = Omit< +export type OmitLifecycle<T extends Lifecycle | LifecycleAsync | object> = Omit< T, 'dispose' | 'dispose$' | 'disposed' >; diff --git a/code/sys/types/src/types/t.Dom.ts b/code/sys/types/src/types/t.Dom.ts index d82e6b19ec..231cca7511 100644 --- a/code/sys/types/src/types/t.Dom.ts +++ b/code/sys/types/src/types/t.Dom.ts @@ -12,3 +12,6 @@ export type DomRect = { bottom: number; left: number; }; + +/** Readonly version of the DomRect type. */ +export type DomRectReadonly = Readonly<DomRect>; diff --git a/code/sys/types/src/types/t.Immutable.ts b/code/sys/types/src/types/t.Immutable.ts index ad7ded5cab..25c9806b26 100644 --- a/code/sys/types/src/types/t.Immutable.ts +++ b/code/sys/types/src/types/t.Immutable.ts @@ -55,6 +55,15 @@ export type ImmutableEvents< C extends ImmutableChange<D, P> = ImmutableChange<D, P>, > = t.Lifecycle & { readonly changed$: t.Observable<C> }; +/** + * Utility type to infer the event-type contained within the ImmutableEvents type. + */ +export type InferImmutableEvent<T extends { changed$: t.Observable<any> }> = T extends { + changed$: t.Observable<infer E>; +} + ? E + : never; + /** * Represents a before/after patched change to the immutable state. */ diff --git a/code/sys/types/src/types/t.Number.ts b/code/sys/types/src/types/t.Number.ts index 0fc24f1180..96debd733c 100644 --- a/code/sys/types/src/types/t.Number.ts +++ b/code/sys/types/src/types/t.Number.ts @@ -1,29 +1,26 @@ -/** - * A number representing an 0-based index. - */ +/** A number representing an 0-based index. */ export type Index = number; -/** - * A number representing a network port. - */ +/** A number representing a network port. */ export type PortNumber = number; -/** - * A number representing screen pixels. - */ +/** A number representing screen pixels. */ export type Pixels = number; -/** - * Number representing a percentage: 0..1 ā (0=0%, 1=100%). - */ +/** Number representing a percentage: 0..1 ā (0=0%, 1=100%). */ export type Percent = number; -/** - * A pixel OR a percentage number: 0..1 = percent, >1 = pixels. - */ +/** A pixel OR a percentage number: 0..1 = percent, >1 = pixels. */ export type PixelOrPercent = Pixels | Percent; // -/** - * A number that represents a "total" (typically 1-based). - */ +/** A number that represents an "offset" of another value. */ +export type NumberOffset = number; + +/** A number that represents a "total" (typically 1-based). */ export type NumberTotal = number; + +/** A number that represents a width. */ +export type NumberWidth = number; + +/** A number that represents a height. */ +export type NumberHeight = number; diff --git a/code/sys/types/src/types/t.Pkg.dist.ts b/code/sys/types/src/types/t.Pkg.dist.ts index 1f180ed427..17f17ea1f0 100644 --- a/code/sys/types/src/types/t.Pkg.dist.ts +++ b/code/sys/types/src/types/t.Pkg.dist.ts @@ -7,6 +7,8 @@ import type { t } from './common.ts'; * produced during a build/bundle operation for a module. */ export type DistPkg = { + '-type:': 'jsr:@sys/types:DistPkg'; + /** The package meta-data info. */ pkg: t.Pkg; diff --git a/code/sys/types/src/types/t.String.ts b/code/sys/types/src/types/t.String.ts index 4592276cb5..f96d1996a0 100644 --- a/code/sys/types/src/types/t.String.ts +++ b/code/sys/types/src/types/t.String.ts @@ -64,6 +64,9 @@ export type StringJson = string; /** A raw string of unparsed YAML. */ export type StringYaml = string; +/** String representing a timestamp in the form "HH:MM:SS:mmm". */ +export type StringTimestamp = string; + /** * The name (module-specifier) of an ESM import. * eg: diff --git a/code/sys/types/src/types/t.Time.ts b/code/sys/types/src/types/t.Time.ts index 594ded265a..4d463be44a 100644 --- a/code/sys/types/src/types/t.Time.ts +++ b/code/sys/types/src/types/t.Time.ts @@ -1,27 +1,34 @@ +import type { t } from './common.ts'; + +/** + * A number representing a time (eg. msecs, secs etc). + */ +export type NumberTime = number; + /** * A number representing milliseconds. */ -export type Msecs = number; +export type Msecs = NumberTime; /** * Number represening seconds. */ -export type Secs = number; +export type Secs = NumberTime; /** * Number represening minutes. */ -export type Mins = number; +export type Mins = NumberTime; /** * Number represening hours. */ -export type Hours = number; +export type Hours = NumberTime; /** * Number represening days. */ -export type Days = number; +export type Days = NumberTime; /** * Represents a Unix Epoch timestamp in seconds. The Unix Epoch time is @@ -52,3 +59,19 @@ export type TimeDuration = { readonly day: Days; toString(unit?: TimeUnit | { unit?: TimeUnit; round?: number }): string; }; + +/** + * A index/map of timestamp related data-objects. + */ +export type Timestamps<T> = { + [HH_MM_SS_mmm: t.StringTimestamp]: T; +}; + +/** + * A single timestamp with data and duration props. + */ +export type Timestamp<T> = { + timestamp: t.StringTimestamp; + total: t.TimeDuration; + data: T; +}; diff --git a/deno.json b/deno.json index 700859c013..55324a8f9b 100644 --- a/deno.json +++ b/deno.json @@ -12,8 +12,9 @@ "reload:clear": "rm -rf node_modules", "reload:install": "deno install --reload", - "prep": "deno run -RWE --allow-ffi --allow-run ./scripts/main.ts --prep", - "bump": "deno run -RWE --allow-sys --allow-ffi --allow-run ./scripts/main.ts --bump", + "prep": "deno run -RWE --allow-ffi --allow-run ./scripts/main.ts --prep", + "bump": "deno run -RWE --allow-sys --allow-ffi --allow-run ./scripts/main.ts --bump", + "tmpl": "deno run -RWE ./scripts/main.ts --tmpl", "tmp": "deno run -A ./scripts/-tmp.ts" }, @@ -25,7 +26,7 @@ "jsxImportSource": "react" }, "nodeModulesDir": "auto", - "importMap": "./deno.imports.json", + "importMap": "./imports.json", "workspace": [ "./code/sys/types", "./code/sys/std", @@ -63,8 +64,8 @@ "./code/sys/main", "./code/-samples/deno.vite-react", + "./deploy/@tdb.slc", "./deploy/api.db.team", - "./deploy/slc.db.team", "./deploy/tmp.db.team", "./code/-tmpl/deno", diff --git a/deno.lock b/deno.lock index 519c656b1b..9dfa4b3c69 100644 --- a/deno.lock +++ b/deno.lock @@ -7,48 +7,30 @@ "jsr:@cliffy/keypress@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/prompt@1.0.0-rc.7": "1.0.0-rc.7", "jsr:@cliffy/table@1.0.0-rc.7": "1.0.0-rc.7", - "jsr:@deno/cache-dir@0.13.2": "0.13.2", - "jsr:@deno/emit@*": "0.46.0", - "jsr:@deno/graph@~0.73.1": "0.73.1", - "jsr:@std/assert@0.223": "0.223.0", - "jsr:@std/assert@^1.0.10": "1.0.11", "jsr:@std/assert@~1.0.6": "1.0.11", "jsr:@std/async@1.0.10": "1.0.10", - "jsr:@std/bytes@0.223": "0.223.0", - "jsr:@std/bytes@^1.0.2": "1.0.4", - "jsr:@std/crypto@^1.0.3": "1.0.3", - "jsr:@std/data-structures@^1.0.6": "1.0.6", "jsr:@std/datetime@0.225.3": "0.225.3", "jsr:@std/dotenv@0.225.3": "0.225.3", "jsr:@std/encoding@1.0.7": "1.0.7", "jsr:@std/encoding@~1.0.5": "1.0.7", - "jsr:@std/fmt@0.223": "0.223.0", - "jsr:@std/fmt@~1.0.2": "1.0.5", - "jsr:@std/fs@0.223": "0.223.0", - "jsr:@std/fs@1.0.11": "1.0.11", + "jsr:@std/fmt@~1.0.2": "1.0.6", "jsr:@std/fs@1.0.13": "1.0.13", - "jsr:@std/fs@^1.0.9": "1.0.11", - "jsr:@std/internal@^1.0.5": "1.0.5", - "jsr:@std/io@0.223": "0.223.0", "jsr:@std/io@~0.224.9": "0.224.9", - "jsr:@std/path@0.223": "0.223.0", "jsr:@std/path@1.0.8": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/path@~1.0.6": "1.0.8", "jsr:@std/semver@1.0.4": "1.0.4", - "jsr:@std/testing@1.0.9": "1.0.9", - "jsr:@std/text@~1.0.7": "1.0.10", - "jsr:@std/uuid@1.0.4": "1.0.4", + "jsr:@std/text@~1.0.7": "1.0.11", "npm:@automerge/automerge-repo-network-broadcastchannel@1.2.1": "1.2.1", "npm:@automerge/automerge-repo-storage-indexeddb@1.2.1": "1.2.1", "npm:@automerge/automerge-repo-storage-nodefs@1.2.1": "1.2.1", "npm:@automerge/automerge-repo@1.2.1": "1.2.1", "npm:@automerge/automerge@2.2.8": "2.2.8", + "npm:@deno/vite-plugin@1.0.4": "1.0.4_vite@6.1.1", "npm:@jsr/std__async@1.0.10": "1.0.10", "npm:@jsr/std__datetime@0.225.3": "0.225.3", "npm:@jsr/std__dotenv@0.225.3": "0.225.3", "npm:@jsr/std__encoding@1.0.7": "1.0.7", - "npm:@jsr/std__fs@1.0.11": "1.0.11", "npm:@jsr/std__fs@1.0.13": "1.0.13", "npm:@jsr/std__path@1.0.8": "1.0.8", "npm:@jsr/std__semver@1.0.4": "1.0.4", @@ -56,12 +38,15 @@ "npm:@jsr/std__uuid@1.0.4": "1.0.4", "npm:@noble/hashes@1.7.1": "1.7.1", "npm:@onsetsoftware/automerge-patcher@0.14.0": "0.14.0_@automerge+automerge@2.2.8", + "npm:@preact/signals-core@1.8.0": "1.8.0", + "npm:@preact/signals-react@3.0.1": "3.0.1_react@18.3.1", + "npm:@svgdotjs/svg.js@3.2.4": "3.2.4", "npm:@types/diff@7.0.1": "7.0.1", "npm:@types/node@*": "22.12.0", "npm:@types/react-dom@18.3.5": "18.3.5_@types+react@18.3.18", "npm:@types/react@18.3.18": "18.3.18", "npm:@vidstack/react@1.12.12": "1.12.12_@types+react@18.3.18_react@18.3.1", - "npm:@vitejs/plugin-react-swc@3.8.0": "3.8.0_vite@6.1.1__@types+node@22.12.0__yaml@2.7.0_@types+node@22.12.0_yaml@2.7.0", + "npm:@vitejs/plugin-react-swc@3.8.0": "3.8.0_vite@6.1.1", "npm:approx-string-match@2": "2.0.0", "npm:chai@5": "5.2.0", "npm:csstype@3": "3.1.3", @@ -75,6 +60,7 @@ "npm:hono@4.7.2": "4.7.2", "npm:ignore@7": "7.0.3", "npm:immer@10": "10.1.1", + "npm:motion@12.5.0": "12.5.0_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:ollama@0.5.13": "0.5.13", "npm:ora@8.2.0": "8.2.0", "npm:pretty-bytes@6.1.1": "6.1.1", @@ -83,10 +69,9 @@ "npm:react-dom@18.3.1": "18.3.1_react@18.3.1", "npm:react-error-boundary@5": "5.0.0_react@18.3.1", "npm:react-icons@5.5.0": "5.5.0_react@18.3.1", - "npm:react-inspector@6": "6.0.2_react@18.3.1", + "npm:react-inspector@6.0.2": "6.0.2_react@18.3.1", "npm:react-spinners@0.15.0": "0.15.0_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:react@18.3.1": "18.3.1", - "npm:rollup@4.34.6": "4.34.6", "npm:rollup@4.34.8": "4.34.8", "npm:rxjs@7.8.2": "7.8.2", "npm:strip-ansi@7": "7.1.0", @@ -95,11 +80,11 @@ "npm:ts-essentials@10.0.4": "10.0.4", "npm:ua-parser-js@2.0.2": "2.0.2", "npm:valibot@1.0.0-rc.1": "1.0.0-rc.1", - "npm:vite-plugin-wasm@3.4.1": "3.4.1_vite@6.1.1__@types+node@22.12.0__yaml@2.7.0_@types+node@22.12.0_yaml@2.7.0", - "npm:vite@*": "6.1.1_@types+node@22.12.0_yaml@2.7.0", - "npm:vite@6.1.1": "6.1.1_@types+node@22.12.0_yaml@2.7.0", - "npm:vitepress@*": "1.6.3_vite@5.4.14_vue@3.5.13_focus-trap@7.6.4_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1", - "npm:vitepress@1.6.3": "1.6.3_vite@5.4.14_vue@3.5.13_focus-trap@7.6.4_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1", + "npm:vite-plugin-wasm@3.4.1": "3.4.1_vite@6.1.1", + "npm:vite@*": "6.1.1_yaml@2.7.0", + "npm:vite@6.1.1": "6.1.1_yaml@2.7.0", + "npm:vitepress@*": "1.6.3_vite@5.4.14__@types+node@22.12.0_vue@3.5.13_focus-trap@7.6.4_@types+node@22.12.0_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1", + "npm:vitepress@1.6.3": "1.6.3_vite@5.4.14__@types+node@22.12.0_vue@3.5.13_focus-trap@7.6.4_@types+node@22.12.0_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:vue@3.5.13": "3.5.13", "npm:yaml@2.7.0": "2.7.0" }, @@ -109,7 +94,7 @@ "dependencies": [ "jsr:@cliffy/internal", "jsr:@std/encoding@~1.0.5", - "jsr:@std/io@~0.224.9" + "jsr:@std/io" ] }, "@cliffy/internal@1.0.0-rc.7": { @@ -131,9 +116,9 @@ "jsr:@cliffy/ansi", "jsr:@cliffy/internal", "jsr:@cliffy/keycode", - "jsr:@std/assert@~1.0.6", - "jsr:@std/fmt@~1.0.2", - "jsr:@std/io@~0.224.9", + "jsr:@std/assert", + "jsr:@std/fmt", + "jsr:@std/io", "jsr:@std/path@~1.0.6", "jsr:@std/text" ] @@ -141,53 +126,15 @@ "@cliffy/table@1.0.0-rc.7": { "integrity": "9fdd9776eda28a0b397981c400eeb1aa36da2371b43eefe12e6ff555290e3180", "dependencies": [ - "jsr:@std/fmt@~1.0.2" + "jsr:@std/fmt" ] }, - "@deno/cache-dir@0.13.2": { - "integrity": "c22419dfe27ab85f345bee487aaaadba498b005cce3644e9d2528db035c5454d", - "dependencies": [ - "jsr:@deno/graph", - "jsr:@std/fmt@0.223", - "jsr:@std/fs@0.223", - "jsr:@std/io@0.223", - "jsr:@std/path@0.223" - ] - }, - "@deno/emit@0.46.0": { - "integrity": "e276be2c77bac1b93caf775762e2a49a54cb00da2d48ca2b01ed8d7cba9d082c", - "dependencies": [ - "jsr:@deno/cache-dir", - "jsr:@std/path@0.223" - ] - }, - "@deno/graph@0.73.1": { - "integrity": "cd69639d2709d479037d5ce191a422eabe8d71bb68b0098344f6b07411c84d41" - }, - "@std/assert@0.223.0": { - "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" - }, "@std/assert@1.0.11": { - "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", - "dependencies": [ - "jsr:@std/internal" - ] + "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1" }, "@std/async@1.0.10": { "integrity": "2ff1b1c7d33d1416159989b0f69e59ec7ee8cb58510df01e454def2108b3dbec" }, - "@std/bytes@0.223.0": { - "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" - }, - "@std/bytes@1.0.4": { - "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" - }, - "@std/crypto@1.0.3": { - "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f" - }, - "@std/data-structures@1.0.6": { - "integrity": "76a7fd8080c66604c0496220a791860492ab21a04a63a969c0b9a0609bbbb760" - }, "@std/datetime@0.225.3": { "integrity": "fda0b7791b43d9a46764986fec39922a8ceb70ffca642651fa7d0683501a171a" }, @@ -197,20 +144,8 @@ "@std/encoding@1.0.7": { "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" }, - "@std/fmt@0.223.0": { - "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208" - }, - "@std/fmt@1.0.5": { - "integrity": "0cfab43364bc36650d83c425cd6d99910fc20c4576631149f0f987eddede1a4d" - }, - "@std/fs@0.223.0": { - "integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c" - }, - "@std/fs@1.0.11": { - "integrity": "ba674672693340c5ebdd018b4fe1af46cb08741f42b4c538154e97d217b55bdd", - "dependencies": [ - "jsr:@std/path@^1.0.8" - ] + "@std/fmt@1.0.6": { + "integrity": "a2c56a69a2369876ddb3ad6a500bb6501b5bad47bb3ea16bfb0c18974d2661fc" }, "@std/fs@1.0.13": { "integrity": "756d3ff0ade91c9e72b228e8012b6ff00c3d4a4ac9c642c4dac083536bf6c605", @@ -218,68 +153,35 @@ "jsr:@std/path@^1.0.8" ] }, - "@std/internal@1.0.5": { - "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" - }, - "@std/io@0.223.0": { - "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", - "dependencies": [ - "jsr:@std/assert@0.223", - "jsr:@std/bytes@0.223" - ] - }, "@std/io@0.224.9": { "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" }, - "@std/path@0.223.0": { - "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989", - "dependencies": [ - "jsr:@std/assert@0.223" - ] - }, "@std/path@1.0.8": { "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" }, "@std/semver@1.0.4": { "integrity": "a62af791917d8fd6c48d6ebbb872f83fad3fc6671ffadbbd39ea229c2d34d175" }, - "@std/testing@1.0.9": { - "integrity": "9bdd4ac07cb13e7594ac30e90f6ceef7254ac83a9aeaa089be0008f33aab5cd4", - "dependencies": [ - "jsr:@std/assert@^1.0.10", - "jsr:@std/data-structures", - "jsr:@std/fs@^1.0.9", - "jsr:@std/internal", - "jsr:@std/path@^1.0.8" - ] - }, - "@std/text@1.0.10": { - "integrity": "9dcab377450253c0efa9a9a0c731040bfd4e1c03f8303b5934381467b7954338" - }, - "@std/uuid@1.0.4": { - "integrity": "f4233149cc8b4753cc3763fd83a7c4101699491f55c7be78dc7b30281946d7a0", - "dependencies": [ - "jsr:@std/bytes@^1.0.2", - "jsr:@std/crypto" - ] + "@std/text@1.0.11": { + "integrity": "f191fa22590cac8b1cdba6cc4ab97940e720f7cc67b3084e54405b428bf5843d" } }, "npm": { - "@algolia/autocomplete-core@1.17.7_algoliasearch@5.20.3": { + "@algolia/autocomplete-core@1.17.7_algoliasearch@5.21.0": { "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", "dependencies": [ "@algolia/autocomplete-plugin-algolia-insights", "@algolia/autocomplete-shared" ] }, - "@algolia/autocomplete-plugin-algolia-insights@1.17.7_search-insights@2.17.3_algoliasearch@5.20.3": { + "@algolia/autocomplete-plugin-algolia-insights@1.17.7_search-insights@2.17.3_algoliasearch@5.21.0": { "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", "dependencies": [ "@algolia/autocomplete-shared", "search-insights" ] }, - "@algolia/autocomplete-preset-algolia@1.17.7_@algolia+client-search@5.20.3_algoliasearch@5.20.3": { + "@algolia/autocomplete-preset-algolia@1.17.7_@algolia+client-search@5.21.0_algoliasearch@5.21.0": { "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", "dependencies": [ "@algolia/autocomplete-shared", @@ -287,15 +189,15 @@ "algoliasearch" ] }, - "@algolia/autocomplete-shared@1.17.7_@algolia+client-search@5.20.3_algoliasearch@5.20.3": { + "@algolia/autocomplete-shared@1.17.7_@algolia+client-search@5.21.0_algoliasearch@5.21.0": { "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", "dependencies": [ "@algolia/client-search", "algoliasearch" ] }, - "@algolia/client-abtesting@5.20.3": { - "integrity": "sha512-wPOzHYSsW+H97JkBLmnlOdJSpbb9mIiuNPycUCV5DgzSkJFaI/OFxXfZXAh1gqxK+hf0miKue1C9bltjWljrNA==", + "@algolia/client-abtesting@5.21.0": { + "integrity": "sha512-I239aSmXa3pXDhp3AWGaIfesqJBNFA7drUM8SIfNxMIzvQXUnHRf4rW1o77QXLI/nIClNsb8KOLaB62gO9LnlQ==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -303,8 +205,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/client-analytics@5.20.3": { - "integrity": "sha512-XE3iduH9lA7iTQacDGofBQyIyIgaX8qbTRRdj1bOCmfzc9b98CoiMwhNwdTifmmMewmN0EhVF3hP8KjKWwX7Yw==", + "@algolia/client-analytics@5.21.0": { + "integrity": "sha512-OxoUfeG9G4VE4gS7B4q65KkHzdGsQsDwxQfR5J9uKB8poSGuNlHJWsF3ABqCkc5VliAR0m8KMjsQ9o/kOpEGnQ==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -312,11 +214,11 @@ "@algolia/requester-node-http" ] }, - "@algolia/client-common@5.20.3": { - "integrity": "sha512-IYRd/A/R3BXeaQVT2805lZEdWo54v39Lqa7ABOxIYnUvX2vvOMW1AyzCuT0U7Q+uPdD4UW48zksUKRixShcWxA==" + "@algolia/client-common@5.21.0": { + "integrity": "sha512-iHLgDQFyZNe9M16vipbx6FGOA8NoMswHrfom/QlCGoyh7ntjGvfMb+J2Ss8rRsAlOWluv8h923Ku3QVaB0oWDQ==" }, - "@algolia/client-insights@5.20.3": { - "integrity": "sha512-QGc/bmDUBgzB71rDL6kihI2e1Mx6G6PxYO5Ks84iL3tDcIel1aFuxtRF14P8saGgdIe1B6I6QkpkeIddZ6vWQw==", + "@algolia/client-insights@5.21.0": { + "integrity": "sha512-y7XBO9Iwb75FLDl95AYcWSLIViJTpR5SUUCyKsYhpP9DgyUqWbISqDLXc96TS9shj+H+7VsTKA9cJK8NUfVN6g==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -324,8 +226,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/client-personalization@5.20.3": { - "integrity": "sha512-zuM31VNPDJ1LBIwKbYGz/7+CSm+M8EhlljDamTg8AnDilnCpKjBebWZR5Tftv/FdWSro4tnYGOIz1AURQgZ+tQ==", + "@algolia/client-personalization@5.21.0": { + "integrity": "sha512-6KU658lD9Tss4oCX6c/O15tNZxw7vR+WAUG95YtZzYG/KGJHTpy2uckqbMmC2cEK4a86FAq4pH5azSJ7cGMjuw==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -333,8 +235,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/client-query-suggestions@5.20.3": { - "integrity": "sha512-Nn872PuOI8qzi1bxMMhJ0t2AzVBqN01jbymBQOkypvZHrrjZPso3iTpuuLLo9gi3yc/08vaaWTAwJfPhxPwJUw==", + "@algolia/client-query-suggestions@5.21.0": { + "integrity": "sha512-pG6MyVh1v0X+uwrKHn3U+suHdgJ2C+gug+UGkNHfMELHMsEoWIAQhxMBOFg7hCnWBFjQnuq6qhM3X9X5QO3d9Q==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -342,8 +244,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/client-search@5.20.3": { - "integrity": "sha512-9+Fm1ahV8/2goSIPIqZnVitV5yHW5E5xTdKy33xnqGd45A9yVv5tTkudWzEXsbfBB47j9Xb3uYPZjAvV5RHbKA==", + "@algolia/client-search@5.21.0": { + "integrity": "sha512-nZfgJH4njBK98tFCmCW1VX/ExH4bNOl9DSboxeXGgvhoL0fG1+4DDr/mrLe21OggVCQqHwXBMh6fFInvBeyhiQ==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -351,8 +253,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/ingestion@1.20.3": { - "integrity": "sha512-5GHNTiZ3saLjTNyr6WkP5hzDg2eFFAYWomvPcm9eHWskjzXt8R0IOiW9kkTS6I6hXBwN5H9Zna5mZDSqqJdg+g==", + "@algolia/ingestion@1.21.0": { + "integrity": "sha512-k6MZxLbZphGN5uRri9J/krQQBjUrqNcScPh985XXEFXbSCRvOPKVtjjLdVjGVHXXPOQgKrIZHxIdRNbHS+wVuA==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -360,8 +262,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/monitoring@1.20.3": { - "integrity": "sha512-KUWQbTPoRjP37ivXSQ1+lWMfaifCCMzTnEcEnXwAmherS5Tp7us6BAqQDMGOD4E7xyaS2I8pto6WlOzxH+CxmA==", + "@algolia/monitoring@1.21.0": { + "integrity": "sha512-FiW5nnmyHvaGdorqLClw3PM6keXexAMiwbwJ9xzQr4LcNefLG3ln82NafRPgJO/z0dETAOKjds5aSmEFMiITHQ==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -369,8 +271,8 @@ "@algolia/requester-node-http" ] }, - "@algolia/recommend@5.20.3": { - "integrity": "sha512-oo/gG77xTTTclkrdFem0Kmx5+iSRFiwuRRdxZETDjwzCI7svutdbwBgV/Vy4D4QpYaX4nhY/P43k84uEowCE4Q==", + "@algolia/recommend@5.21.0": { + "integrity": "sha512-+JXavbbliaLmah5QNgc/TDW/+r0ALa+rGhg5Y7+pF6GpNnzO0L+nlUaDNE8QbiJfz54F9BkwFUnJJeRJAuzTFw==", "dependencies": [ "@algolia/client-common", "@algolia/requester-browser-xhr", @@ -378,20 +280,20 @@ "@algolia/requester-node-http" ] }, - "@algolia/requester-browser-xhr@5.20.3": { - "integrity": "sha512-BkkW7otbiI/Er1AiEPZs1h7lxbtSO9p09jFhv3/iT8/0Yz0CY79VJ9iq+Wv1+dq/l0OxnMpBy8mozrieGA3mXQ==", + "@algolia/requester-browser-xhr@5.21.0": { + "integrity": "sha512-Iw+Yj5hOmo/iixHS94vEAQ3zi5GPpJywhfxn1el/zWo4AvPIte/+1h9Ywgw/+3M7YBj4jgAkScxjxQCxzLBsjA==", "dependencies": [ "@algolia/client-common" ] }, - "@algolia/requester-fetch@5.20.3": { - "integrity": "sha512-eAVlXz7UNzTsA1EDr+p0nlIH7WFxo7k3NMxYe8p38DH8YVWLgm2MgOVFUMNg9HCi6ZNOi/A2w/id2ZZ4sKgUOw==", + "@algolia/requester-fetch@5.21.0": { + "integrity": "sha512-Z00SRLlIFj3SjYVfsd9Yd3kB3dUwQFAkQG18NunWP7cix2ezXpJqA+xAoEf9vc4QZHdxU3Gm8gHAtRiM2iVaTQ==", "dependencies": [ "@algolia/client-common" ] }, - "@algolia/requester-node-http@5.20.3": { - "integrity": "sha512-FqR3pQPfHfQyX1wgcdK6iyqu86yP76MZd4Pzj1y/YLMj9rRmRCY0E0AffKr//nrOFEwv6uY8BQY4fd9/6b0ZCg==", + "@algolia/requester-node-http@5.21.0": { + "integrity": "sha512-WqU0VumUILrIeVYCTGZlyyZoC/tbvhiyPxfGRRO1cSjxN558bnJLlR2BvS0SJ5b75dRNK7HDvtXo2QoP9eLfiA==", "dependencies": [ "@algolia/client-common" ] @@ -442,20 +344,20 @@ "@babel/helper-validator-identifier@7.25.9": { "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" }, - "@babel/parser@7.26.9": { - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "@babel/parser@7.26.10": { + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dependencies": [ "@babel/types" ] }, - "@babel/runtime@7.26.9": { - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "@babel/runtime@7.26.10": { + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dependencies": [ "regenerator-runtime" ] }, - "@babel/types@7.26.9": { - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "@babel/types@7.26.10": { + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dependencies": [ "@babel/helper-string-parser", "@babel/helper-validator-identifier" @@ -485,6 +387,12 @@ "@jridgewell/trace-mapping" ] }, + "@deno/vite-plugin@1.0.4_vite@6.1.1": { + "integrity": "sha512-xg8YT8Wn2sGXSnJgiGTpBGX1Dov0c6fd1rAp8VsfrCUtyBRRWzwVMAnd3fQ4yq8h7LSVvJUxEFN4U421k/DQLA==", + "dependencies": [ + "vite@6.1.1" + ] + }, "@docsearch/css@3.8.2": { "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==" }, @@ -495,7 +403,7 @@ "preact" ] }, - "@docsearch/react@3.8.2_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1_algoliasearch@5.20.3": { + "@docsearch/react@3.8.2_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1_algoliasearch@5.21.0": { "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", "dependencies": [ "@algolia/autocomplete-core", @@ -667,8 +575,8 @@ "@floating-ui/utils@0.2.9": { "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, - "@iconify-json/simple-icons@1.2.25": { - "integrity": "sha512-2E1/gOCO97rF6usfhhiXxwzCb+UhdEsxW3lW1Sew+xZY0COY6dp82Z/r1rUt2fWKneWjuoGcNeJHHXQyG8mIuw==", + "@iconify-json/simple-icons@1.2.29": { + "integrity": "sha512-KYrxmxtRz6iOAulRiUsIBMUuXek+H+Evwf8UvYPIkbQ+KDoOqTegHx3q/w3GDDVC0qJYB+D3hXPMZcpm78qIuA==", "dependencies": [ "@iconify/types" ] @@ -727,12 +635,6 @@ "@jsr/std__encoding@1.0.7": { "integrity": "sha512-eySTZkCAHjiKGgOMvhUyTL4aT22svaCO0eO6CAbz48kRECMtRGEfe3KWXkVy4fhlZ/4OTw/RQLrTYyEqpGQp/Q==" }, - "@jsr/std__fs@1.0.11": { - "integrity": "sha512-Ynnw/D9hhsgeooOY4ak5oPgm9P7kXK/L2wNyWkDMfN7Pe5c9jtg2REkANhQU7axpp4Sct39/2/fayXP5FIfTcg==", - "dependencies": [ - "@jsr/std__path" - ] - }, "@jsr/std__fs@1.0.13": { "integrity": "sha512-3hvGsQwDd0ljtwaqG2psox4W0/t8vf0U1MQi6qczWoqajhZ3kgfRv7YexIZHCgWn7IP7nlRbARwhKbStJP7PmQ==", "dependencies": [ @@ -754,7 +656,7 @@ "@jsr/std__assert", "@jsr/std__async", "@jsr/std__data-structures", - "@jsr/std__fs@1.0.11", + "@jsr/std__fs", "@jsr/std__internal", "@jsr/std__path" ] @@ -779,117 +681,71 @@ "@pkgjs/parseargs@0.11.0": { "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" }, - "@rollup/rollup-android-arm-eabi@4.34.6": { - "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==" + "@preact/signals-core@1.8.0": { + "integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==" + }, + "@preact/signals-react@3.0.1_react@18.3.1": { + "integrity": "sha512-HkM5Q2IsETO1M0bUzy4JB0EPQCf99SMbWP9K6GYlYVHfOX1HLKJ6Dl9L1/1rQnmrQpUsTw0R+IJQDh6tYWar2g==", + "dependencies": [ + "@preact/signals-core", + "react", + "use-sync-external-store" + ] }, "@rollup/rollup-android-arm-eabi@4.34.8": { "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==" }, - "@rollup/rollup-android-arm64@4.34.6": { - "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==" - }, "@rollup/rollup-android-arm64@4.34.8": { "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==" }, - "@rollup/rollup-darwin-arm64@4.34.6": { - "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==" - }, "@rollup/rollup-darwin-arm64@4.34.8": { "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==" }, - "@rollup/rollup-darwin-x64@4.34.6": { - "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==" - }, "@rollup/rollup-darwin-x64@4.34.8": { "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==" }, - "@rollup/rollup-freebsd-arm64@4.34.6": { - "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==" - }, "@rollup/rollup-freebsd-arm64@4.34.8": { "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==" }, - "@rollup/rollup-freebsd-x64@4.34.6": { - "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==" - }, "@rollup/rollup-freebsd-x64@4.34.8": { "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==" }, - "@rollup/rollup-linux-arm-gnueabihf@4.34.6": { - "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==" - }, "@rollup/rollup-linux-arm-gnueabihf@4.34.8": { "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==" }, - "@rollup/rollup-linux-arm-musleabihf@4.34.6": { - "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==" - }, "@rollup/rollup-linux-arm-musleabihf@4.34.8": { "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==" }, - "@rollup/rollup-linux-arm64-gnu@4.34.6": { - "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==" - }, "@rollup/rollup-linux-arm64-gnu@4.34.8": { "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==" }, - "@rollup/rollup-linux-arm64-musl@4.34.6": { - "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==" - }, "@rollup/rollup-linux-arm64-musl@4.34.8": { "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==" }, - "@rollup/rollup-linux-loongarch64-gnu@4.34.6": { - "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==" - }, "@rollup/rollup-linux-loongarch64-gnu@4.34.8": { "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==" }, - "@rollup/rollup-linux-powerpc64le-gnu@4.34.6": { - "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==" - }, "@rollup/rollup-linux-powerpc64le-gnu@4.34.8": { "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==" }, - "@rollup/rollup-linux-riscv64-gnu@4.34.6": { - "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==" - }, "@rollup/rollup-linux-riscv64-gnu@4.34.8": { "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==" }, - "@rollup/rollup-linux-s390x-gnu@4.34.6": { - "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==" - }, "@rollup/rollup-linux-s390x-gnu@4.34.8": { "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==" }, - "@rollup/rollup-linux-x64-gnu@4.34.6": { - "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==" - }, "@rollup/rollup-linux-x64-gnu@4.34.8": { "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==" }, - "@rollup/rollup-linux-x64-musl@4.34.6": { - "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==" - }, "@rollup/rollup-linux-x64-musl@4.34.8": { "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==" }, - "@rollup/rollup-win32-arm64-msvc@4.34.6": { - "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==" - }, "@rollup/rollup-win32-arm64-msvc@4.34.8": { "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==" }, - "@rollup/rollup-win32-ia32-msvc@4.34.6": { - "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==" - }, "@rollup/rollup-win32-ia32-msvc@4.34.8": { "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==" }, - "@rollup/rollup-win32-x64-msvc@4.34.6": { - "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==" - }, "@rollup/rollup-win32-x64-msvc@4.34.8": { "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==" }, @@ -948,38 +804,41 @@ "@shikijs/vscode-textmate@10.0.2": { "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" }, - "@swc/core-darwin-arm64@1.10.18": { - "integrity": "sha512-FdGqzAIKVQJu8ROlnHElP59XAUsUzCFSNsou+tY/9ba+lhu8R9v0OI5wXiPErrKGZpQFMmx/BPqqhx3X4SuGNg==" + "@svgdotjs/svg.js@3.2.4": { + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==" }, - "@swc/core-darwin-x64@1.10.18": { - "integrity": "sha512-RZ73gZRituL/ZVLgrW6BYnQ5g8tuStG4cLUiPGJsUZpUm0ullSH6lHFvZTCBNFTfpQChG6eEhi2IdG6DwFp1lw==" + "@swc/core-darwin-arm64@1.11.11": { + "integrity": "sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==" }, - "@swc/core-linux-arm-gnueabihf@1.10.18": { - "integrity": "sha512-8iJqI3EkxJuuq21UHoen1VS+QlS23RvynRuk95K+Q2HBjygetztCGGEc+Xelx9a0uPkDaaAtFvds4JMDqb9SAA==" + "@swc/core-darwin-x64@1.11.11": { + "integrity": "sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==" }, - "@swc/core-linux-arm64-gnu@1.10.18": { - "integrity": "sha512-8f1kSktWzMB6PG+r8lOlCfXz5E8Qhsmfwonn77T/OfjvGwQaWrcoASh2cdjpk3dydbf8jsKGPQE1lSc7GyjXRQ==" + "@swc/core-linux-arm-gnueabihf@1.11.11": { + "integrity": "sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==" }, - "@swc/core-linux-arm64-musl@1.10.18": { - "integrity": "sha512-4rv+E4VLdgQw6zjbTAauCAEExxChvxMpBUMCiZweTNPKbJJ2dY6BX2WGJ1ea8+RcgqR/Xysj3AFbOz1LBz6dGA==" + "@swc/core-linux-arm64-gnu@1.11.11": { + "integrity": "sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==" }, - "@swc/core-linux-x64-gnu@1.10.18": { - "integrity": "sha512-vTNmyRBVP+sZca+vtwygYPGTNudTU6Gl6XhaZZ7cEUTBr8xvSTgEmYXoK/2uzyXpaTUI4Bmtp1x81cGN0mMoLQ==" + "@swc/core-linux-arm64-musl@1.11.11": { + "integrity": "sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==" }, - "@swc/core-linux-x64-musl@1.10.18": { - "integrity": "sha512-1TZPReKhFCeX776XaT6wegknfg+g3zODve+r4oslFHI+g7cInfWlxoGNDS3niPKyuafgCdOjme2g3OF+zzxfsQ==" + "@swc/core-linux-x64-gnu@1.11.11": { + "integrity": "sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==" }, - "@swc/core-win32-arm64-msvc@1.10.18": { - "integrity": "sha512-o/2CsaWSN3bkzVQ6DA+BiFKSVEYvhWGA1h+wnL2zWmIDs2Knag54sOEXZkCaf8YQyZesGeXJtPEy9hh/vjJgkA==" + "@swc/core-linux-x64-musl@1.11.11": { + "integrity": "sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==" }, - "@swc/core-win32-ia32-msvc@1.10.18": { - "integrity": "sha512-eTPASeJtk4mJDfWiYEiOC6OYUi/N7meHbNHcU8e+aKABonhXrIo/FmnTE8vsUtC6+jakT1TQBdiQ8fzJ1kJVwA==" + "@swc/core-win32-arm64-msvc@1.11.11": { + "integrity": "sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==" }, - "@swc/core-win32-x64-msvc@1.10.18": { - "integrity": "sha512-1Dud8CDBnc34wkBOboFBQud9YlV1bcIQtKSg7zC8LtwR3h+XAaCayZPkpGmmAlCv1DLQPvkF+s0JcaVC9mfffQ==" + "@swc/core-win32-ia32-msvc@1.11.11": { + "integrity": "sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==" }, - "@swc/core@1.10.18": { - "integrity": "sha512-IUWKD6uQYGRy8w2X9EZrtYg1O3SCijlHbCXzMaHQYc1X7yjijQh4H3IVL9ssZZyVp2ZDfQZu4bD5DWxxvpyjvg==", + "@swc/core-win32-x64-msvc@1.11.11": { + "integrity": "sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==" + }, + "@swc/core@1.11.11": { + "integrity": "sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==", "dependencies": [ "@swc/core-darwin-arm64", "@swc/core-darwin-x64", @@ -998,8 +857,8 @@ "@swc/counter@0.1.3": { "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, - "@swc/types@0.1.17": { - "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", + "@swc/types@0.1.19": { + "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", "dependencies": [ "@swc/counter" ] @@ -1054,8 +913,8 @@ "form-data" ] }, - "@types/node@18.19.76": { - "integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==", + "@types/node@18.19.80": { + "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", "dependencies": [ "undici-types@5.26.5" ] @@ -1085,8 +944,8 @@ "@types/unist@3.0.3": { "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, - "@types/web-bluetooth@0.0.20": { - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" + "@types/web-bluetooth@0.0.21": { + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" }, "@ungap/structured-clone@1.3.0": { "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" @@ -1100,15 +959,22 @@ "react" ] }, - "@vitejs/plugin-react-swc@3.8.0_vite@6.1.1__@types+node@22.12.0__yaml@2.7.0_@types+node@22.12.0_yaml@2.7.0": { + "@vitejs/plugin-react-swc@3.8.0_vite@6.1.1": { "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", "dependencies": [ "@swc/core", - "vite@6.1.1_@types+node@22.12.0_yaml@2.7.0" + "vite@6.1.1" + ] + }, + "@vitejs/plugin-vue@5.2.3_vite@5.4.14__@types+node@22.12.0_vue@3.5.13_@types+node@22.12.0": { + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "dependencies": [ + "vite@5.4.14_@types+node@22.12.0", + "vue" ] }, - "@vitejs/plugin-vue@5.2.1_vite@5.4.14_vue@3.5.13": { - "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "@vitejs/plugin-vue@5.2.3_vite@5.4.14_vue@3.5.13": { + "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", "dependencies": [ "vite@5.4.14", "vue" @@ -1209,8 +1075,8 @@ "@vue/shared@3.5.13": { "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" }, - "@vueuse/core@12.7.0": { - "integrity": "sha512-jtK5B7YjZXmkGNHjviyGO4s3ZtEhbzSgrbX+s5o+Lr8i2nYqNyHuPVOeTdM1/hZ5Tkxg/KktAuAVDDiHMraMVA==", + "@vueuse/core@12.8.2": { + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", "dependencies": [ "@types/web-bluetooth", "@vueuse/metadata", @@ -1218,8 +1084,8 @@ "vue" ] }, - "@vueuse/integrations@12.7.0_focus-trap@7.6.4": { - "integrity": "sha512-IEq7K4bCl7mn3uKJaWtNXnd1CAPaHLUMuyj5K1/k/pVcItt0VONZW8xiGxdIovJcQjkzOHjImhX5t6gija+0/g==", + "@vueuse/integrations@12.8.2_focus-trap@7.6.4": { + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", "dependencies": [ "@vueuse/core", "@vueuse/shared", @@ -1227,11 +1093,11 @@ "vue" ] }, - "@vueuse/metadata@12.7.0": { - "integrity": "sha512-4VvTH9mrjXqFN5LYa5YfqHVRI6j7R00Vy4995Rw7PQxyCL3z0Lli86iN4UemWqixxEvYfRjG+hF9wL8oLOn+3g==" + "@vueuse/metadata@12.8.2": { + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==" }, - "@vueuse/shared@12.7.0": { - "integrity": "sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==", + "@vueuse/shared@12.8.2": { + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", "dependencies": [ "vue" ] @@ -1248,8 +1114,8 @@ "acorn" ] }, - "acorn@8.14.0": { - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==" + "acorn@8.14.1": { + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" }, "agentkeepalive@4.6.0": { "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", @@ -1257,8 +1123,8 @@ "humanize-ms" ] }, - "algoliasearch@5.20.3": { - "integrity": "sha512-iNC6BGvipaalFfDfDnXUje8GUlW5asj0cTMsZJwO/0rhsyLx1L7GZFAY8wW+eQ6AM4Yge2p5GSE5hrBlfSD90Q==", + "algoliasearch@5.21.0": { + "integrity": "sha512-hexLq2lSO1K5SW9j21Ubc+q9Ptx7dyRTY7se19U8lhIlVMLCNXWCyQ6C22p9ez8ccX0v7QVmwkl2l1CnuGoO2Q==", "dependencies": [ "@algolia/client-abtesting", "@algolia/client-analytics", @@ -1308,8 +1174,8 @@ "base-64@0.1.0": { "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" }, - "base-x@4.0.0": { - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "base-x@4.0.1": { + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" }, "birpc@0.2.19": { "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==" @@ -1608,8 +1474,8 @@ "tabbable" ] }, - "foreground-child@3.3.0": { - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "foreground-child@3.3.1": { + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dependencies": [ "cross-spawn", "signal-exit" @@ -1634,6 +1500,16 @@ "web-streams-polyfill@4.0.0-beta.3" ] }, + "framer-motion@12.5.0_react@18.3.1_react-dom@18.3.1__react@18.3.1": { + "integrity": "sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA==", + "dependencies": [ + "motion-dom", + "motion-utils", + "react", + "react-dom", + "tslib" + ] + }, "fsevents@2.3.3": { "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" }, @@ -1643,8 +1519,8 @@ "get-east-asian-width@1.3.0": { "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==" }, - "get-intrinsic@1.2.7": { - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "get-intrinsic@1.3.0": { + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": [ "call-bind-apply-helpers", "es-define-property", @@ -1864,8 +1740,8 @@ "micromark-util-symbol@2.0.1": { "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" }, - "micromark-util-types@2.0.1": { - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==" + "micromark-util-types@2.0.2": { + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" }, "mime-db@1.52.0": { "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" @@ -1894,11 +1770,29 @@ "mitt@3.0.1": { "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" }, + "motion-dom@12.5.0": { + "integrity": "sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==", + "dependencies": [ + "motion-utils" + ] + }, + "motion-utils@12.5.0": { + "integrity": "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==" + }, + "motion@12.5.0_react@18.3.1_react-dom@18.3.1__react@18.3.1": { + "integrity": "sha512-BTAYKszMmTvXSsIyeHNMPSicjWgUA4j7OmZv1xPpthm4sPub3ch66fy9U7BhJ1uXNL3YeprsIegzuvps3FkEMw==", + "dependencies": [ + "framer-motion", + "react", + "react-dom", + "tslib" + ] + }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "nanoid@3.3.8": { - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, "node-domexception@1.0.0": { "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" @@ -1979,8 +1873,8 @@ "source-map-js" ] }, - "preact@10.26.2": { - "integrity": "sha512-0gNmv4qpS9HaN3+40CLBAnKe0ZfyE4ZWo5xKlC1rVrr0ckkEvJvAQqKaHANdFKsGstoxrY4AItZ7kZSGVoVjgg==" + "preact@10.26.4": { + "integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==" }, "pretty-bytes@6.1.1": { "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==" @@ -2068,54 +1962,28 @@ "glob" ] }, - "rollup@4.34.6": { - "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==", - "dependencies": [ - "@rollup/rollup-android-arm-eabi@4.34.6", - "@rollup/rollup-android-arm64@4.34.6", - "@rollup/rollup-darwin-arm64@4.34.6", - "@rollup/rollup-darwin-x64@4.34.6", - "@rollup/rollup-freebsd-arm64@4.34.6", - "@rollup/rollup-freebsd-x64@4.34.6", - "@rollup/rollup-linux-arm-gnueabihf@4.34.6", - "@rollup/rollup-linux-arm-musleabihf@4.34.6", - "@rollup/rollup-linux-arm64-gnu@4.34.6", - "@rollup/rollup-linux-arm64-musl@4.34.6", - "@rollup/rollup-linux-loongarch64-gnu@4.34.6", - "@rollup/rollup-linux-powerpc64le-gnu@4.34.6", - "@rollup/rollup-linux-riscv64-gnu@4.34.6", - "@rollup/rollup-linux-s390x-gnu@4.34.6", - "@rollup/rollup-linux-x64-gnu@4.34.6", - "@rollup/rollup-linux-x64-musl@4.34.6", - "@rollup/rollup-win32-arm64-msvc@4.34.6", - "@rollup/rollup-win32-ia32-msvc@4.34.6", - "@rollup/rollup-win32-x64-msvc@4.34.6", - "@types/estree", - "fsevents" - ] - }, "rollup@4.34.8": { "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", "dependencies": [ - "@rollup/rollup-android-arm-eabi@4.34.8", - "@rollup/rollup-android-arm64@4.34.8", - "@rollup/rollup-darwin-arm64@4.34.8", - "@rollup/rollup-darwin-x64@4.34.8", - "@rollup/rollup-freebsd-arm64@4.34.8", - "@rollup/rollup-freebsd-x64@4.34.8", - "@rollup/rollup-linux-arm-gnueabihf@4.34.8", - "@rollup/rollup-linux-arm-musleabihf@4.34.8", - "@rollup/rollup-linux-arm64-gnu@4.34.8", - "@rollup/rollup-linux-arm64-musl@4.34.8", - "@rollup/rollup-linux-loongarch64-gnu@4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu@4.34.8", - "@rollup/rollup-linux-riscv64-gnu@4.34.8", - "@rollup/rollup-linux-s390x-gnu@4.34.8", - "@rollup/rollup-linux-x64-gnu@4.34.8", - "@rollup/rollup-linux-x64-musl@4.34.8", - "@rollup/rollup-win32-arm64-msvc@4.34.8", - "@rollup/rollup-win32-ia32-msvc@4.34.8", - "@rollup/rollup-win32-x64-msvc@4.34.8", + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loongarch64-gnu", + "@rollup/rollup-linux-powerpc64le-gnu", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-msvc", "@types/estree", "fsevents" ] @@ -2218,7 +2086,7 @@ "subhosting@0.1.0-alpha.1": { "integrity": "sha512-uJGozPd5gcUcJVqBjGatFjbPlBfs3osfSFhhcpvl4FhqAZ1mqUoL6fbF1l+HAN74Z5+wOWmkowxguJznp6DKRA==", "dependencies": [ - "@types/node@18.19.76", + "@types/node@18.19.80", "@types/node-fetch", "abort-controller", "agentkeepalive", @@ -2253,7 +2121,7 @@ "ts-essentials@10.0.4": { "integrity": "sha512-lwYdz28+S4nicm+jFi6V58LaAIpxzhg9rLdgNC1VsdP/xiFBseGhF1M/shwCk6zMmwahBZdXcl34LVHrEang3A==" }, - "ts-node@10.9.2_@types+node@22.12.0_typescript@5.7.3": { + "ts-node@10.9.2_@types+node@22.12.0_typescript@5.8.2": { "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dependencies": [ "@cspotcode/source-map-support", @@ -2276,8 +2144,8 @@ "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "typescript@5.7.3": { - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" + "typescript@5.8.2": { + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==" }, "ua-is-frozen@0.1.2": { "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==" @@ -2331,6 +2199,12 @@ "unist-util-visit-parents" ] }, + "use-sync-external-store@1.4.0_react@18.3.1": { + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "dependencies": [ + "react" + ] + }, "uuid@9.0.1": { "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, @@ -2354,10 +2228,10 @@ "vfile-message" ] }, - "vite-plugin-wasm@3.4.1_vite@6.1.1__@types+node@22.12.0__yaml@2.7.0_@types+node@22.12.0_yaml@2.7.0": { + "vite-plugin-wasm@3.4.1_vite@6.1.1": { "integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==", "dependencies": [ - "vite@6.1.1_@types+node@22.12.0_yaml@2.7.0" + "vite@6.1.1" ] }, "vite@5.4.14": { @@ -2366,20 +2240,61 @@ "esbuild@0.21.5", "fsevents", "postcss", - "rollup@4.34.6" + "rollup" ] }, - "vite@6.1.1_@types+node@22.12.0_yaml@2.7.0": { - "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", + "vite@5.4.14_@types+node@22.12.0": { + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dependencies": [ "@types/node@22.12.0", + "esbuild@0.21.5", + "fsevents", + "postcss", + "rollup" + ] + }, + "vite@6.1.1": { + "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", + "dependencies": [ + "esbuild@0.24.2", + "fsevents", + "postcss", + "rollup" + ] + }, + "vite@6.1.1_yaml@2.7.0": { + "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", + "dependencies": [ "esbuild@0.24.2", "fsevents", "postcss", - "rollup@4.34.8", + "rollup", "yaml" ] }, + "vitepress@1.6.3_vite@5.4.14__@types+node@22.12.0_vue@3.5.13_focus-trap@7.6.4_@types+node@22.12.0_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1": { + "integrity": "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==", + "dependencies": [ + "@docsearch/css", + "@docsearch/js", + "@iconify-json/simple-icons", + "@shikijs/core", + "@shikijs/transformers", + "@shikijs/types", + "@types/markdown-it", + "@vitejs/plugin-vue@5.2.3_vite@5.4.14__@types+node@22.12.0_vue@3.5.13_@types+node@22.12.0", + "@vue/devtools-api", + "@vue/shared", + "@vueuse/core", + "@vueuse/integrations", + "focus-trap", + "mark.js", + "minisearch", + "shiki", + "vite@5.4.14_@types+node@22.12.0", + "vue" + ] + }, "vitepress@1.6.3_vite@5.4.14_vue@3.5.13_focus-trap@7.6.4_@types+react@18.3.18_react@18.3.1_react-dom@18.3.1__react@18.3.1": { "integrity": "sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==", "dependencies": [ @@ -2390,7 +2305,7 @@ "@shikijs/transformers", "@shikijs/types", "@types/markdown-it", - "@vitejs/plugin-vue", + "@vitejs/plugin-vue@5.2.3_vite@5.4.14_vue@3.5.13", "@vue/devtools-api", "@vue/shared", "@vueuse/core", @@ -2478,6 +2393,7 @@ "jsr:@cliffy/keypress@1.0.0-rc.7", "jsr:@cliffy/prompt@1.0.0-rc.7", "jsr:@cliffy/table@1.0.0-rc.7", + "jsr:@deno/dnt@0.41.3", "jsr:@std/async@1.0.10", "jsr:@std/datetime@0.225.3", "jsr:@std/dotenv@0.225.3", @@ -2494,6 +2410,9 @@ "npm:@automerge/automerge@2.2.8", "npm:@noble/hashes@1.7.1", "npm:@onsetsoftware/automerge-patcher@0.14.0", + "npm:@preact/signals-core@1.8.0", + "npm:@preact/signals-react@3.0.1", + "npm:@svgdotjs/svg.js@3.2.4", "npm:@types/diff@7.0.1", "npm:approx-string-match@2", "npm:chai@5", @@ -2512,7 +2431,7 @@ "npm:rambda@9.4.2", "npm:ramda@0.30.1", "npm:react-error-boundary@5", - "npm:react-inspector@6", + "npm:react-inspector@6.0.2", "npm:react-spinners@0.15.0", "npm:rxjs@7.8.2", "npm:strip-ansi@7", @@ -2525,6 +2444,7 @@ ], "packageJson": { "dependencies": [ + "npm:@deno/vite-plugin@1.0.4", "npm:@jsr/std__async@1.0.10", "npm:@jsr/std__datetime@0.225.3", "npm:@jsr/std__dotenv@0.225.3", @@ -2540,6 +2460,7 @@ "npm:@vidstack/react@1.12.12", "npm:@vitejs/plugin-react-swc@3.8.0", "npm:hono@4.7.2", + "npm:motion@12.5.0", "npm:react-dom@18.3.1", "npm:react-icons@5.5.0", "npm:react@18.3.1", diff --git a/deploy/@tdb.slc/-scripts/-main.ts b/deploy/@tdb.slc/-scripts/-main.ts new file mode 100644 index 0000000000..7c806709fa --- /dev/null +++ b/deploy/@tdb.slc/-scripts/-main.ts @@ -0,0 +1 @@ +import '../../../code/sys.driver/driver-vite/src/-entry/-main.ts'; diff --git a/deploy/@tdb.slc/-scripts/-tmp.ts b/deploy/@tdb.slc/-scripts/-tmp.ts new file mode 100644 index 0000000000..9e0f27eee7 --- /dev/null +++ b/deploy/@tdb.slc/-scripts/-tmp.ts @@ -0,0 +1 @@ +console.info('š', import.meta.url); diff --git a/deploy/@tdb.slc/README.md b/deploy/@tdb.slc/README.md new file mode 100644 index 0000000000..e892c53866 --- /dev/null +++ b/deploy/@tdb.slc/README.md @@ -0,0 +1,24 @@ +# SLC +- "Social Lean Canvas" UI component library. + +### Description +A shareable common language for modelling āSocial Enterpriseā and āImpact Driven Business.ā The dimensions of āpurposeā and āimpactā are woven into standard business modelling notation (a "canvas"). When done right this provides a clear pathway to simplifying the complexity inherent in building āimpact driven business.ā + + +### Deployments +- staging: https://slc.db.team +- production: https://socialleancanvas.com + +#### Legacy Domains + +``` + socialleancanvas.com +eusic.socialleancanvas.com + socialleancanvas.com/ember-slc +``` + +Prior deployment: +- https://slc-phil-tdb.vercel.app/ +- https://slc-phil-tdb.vercel.app/ember-slc + + diff --git a/deploy/@tdb.slc/deno.json b/deploy/@tdb.slc/deno.json new file mode 100644 index 0000000000..34f6bb2285 --- /dev/null +++ b/deploy/@tdb.slc/deno.json @@ -0,0 +1,24 @@ +{ + "name": "@tdb/slc", + "version": "0.0.1", + "license": "MIT", + "tasks": { + "dev": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=dev --in=./src/-test/index.html", + "build": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=build --in=./src/-test/index.html", + "serve": "deno run -RNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=serve", + "test": "deno test -RWNE --allow-run --allow-ffi --allow-sys", + "dry": "deno publish --allow-dirty --dry-run", + "clean": "deno run -RWE --allow-ffi ./-scripts/-main.ts --cmd=clean", + "upgrade": "deno run -RWNE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=upgrade", + "backup": "deno run -RWE --allow-run --allow-ffi ./-scripts/-main.ts --cmd=backup", + "help": "deno run -RE --allow-ffi ./-scripts/-main.ts --cmd=help", + "tmp": "deno run -A ./-scripts/-tmp.ts" + }, + "exports": { + ".": "./src/mod.ts", + "./t": "./src/types.ts", + "./types": "./src/types.ts", + "./ui": "./src/ui/mod.ts", + "./specs": "./src/-test/entry.Specs.ts" + } +} diff --git a/deploy/@tdb.slc/public/pdf/slc.pdf b/deploy/@tdb.slc/public/pdf/slc.pdf new file mode 100644 index 0000000000..17c0e687e2 Binary files /dev/null and b/deploy/@tdb.slc/public/pdf/slc.pdf differ diff --git a/deploy/@tdb.slc/src/-sample/-test.ui.ts b/deploy/@tdb.slc/src/-sample/-test.ui.ts new file mode 100644 index 0000000000..80e99d3a9c --- /dev/null +++ b/deploy/@tdb.slc/src/-sample/-test.ui.ts @@ -0,0 +1 @@ +export * from '../ui/-test.ui.ts'; diff --git a/deploy/@tdb.slc/src/-sample/common.ts b/deploy/@tdb.slc/src/-sample/common.ts new file mode 100644 index 0000000000..f19dabd006 --- /dev/null +++ b/deploy/@tdb.slc/src/-sample/common.ts @@ -0,0 +1 @@ +export * from '../ui/common.ts'; diff --git a/code/sys.ui/ui-react-devharness/src/m.Dev/-test.ts b/deploy/@tdb.slc/src/-test.ts similarity index 100% rename from code/sys.ui/ui-react-devharness/src/m.Dev/-test.ts rename to deploy/@tdb.slc/src/-test.ts diff --git a/deploy/@tdb.slc/src/-test/base.css b/deploy/@tdb.slc/src/-test/base.css new file mode 100644 index 0000000000..9f57ec5271 --- /dev/null +++ b/deploy/@tdb.slc/src/-test/base.css @@ -0,0 +1,3 @@ +body { + background-color: #293042; /* Inky Dark */ +} diff --git a/deploy/@tdb.slc/src/-test/common.ts b/deploy/@tdb.slc/src/-test/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/deploy/@tdb.slc/src/-test/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/-test/entry.Specs.ts b/deploy/@tdb.slc/src/-test/entry.Specs.ts new file mode 100644 index 0000000000..19919936ae --- /dev/null +++ b/deploy/@tdb.slc/src/-test/entry.Specs.ts @@ -0,0 +1,23 @@ +/** + * @module + * DevHarness visual specs. + */ +import type { t } from './common.ts'; + +export const Specs = { + 'tdb.slc.entry.Landing-1': () => import('../ui/ui.Landing-1/-SPEC.tsx'), + 'tdb.slc.entry.Landing-2': () => import('../ui/ui.Landing-2/-SPEC.tsx'), + 'tdb.slc.entry.Landing-3': () => import('../ui/ui.Landing-3/-SPEC.tsx'), + + 'tdb.slc.ui.Layout': () => import('../ui/ui.Layout/-SPEC.tsx'), + + 'tdb.slc.ui.Logo.Wordmark': () => import('../ui/ui.Logo.Wordmark/-SPEC.tsx'), + 'tdb.slc.ui.Logo.Canvas': () => import('../ui/ui.Logo.Canvas/-SPEC.tsx'), + 'tdb.slc.ui.Video.Background': () => import('../ui/ui.Video.Background/-SPEC.tsx'), + 'tdb.slc.ui.TooSmall': () => import('../ui/ui.TooSmall/-SPEC.tsx'), + + 'tdb.slc.content.videos: (index)': () => import('../ui.Content/-sample/ui.Videos/-SPEC.tsx'), + 'tdb.slc.content.CanvasSlug': () => import('../ui.Content/ui/ui.CanvasSlug/-SPEC.tsx'), + 'tdb.slc.content.FadeText': () => import('../ui.Content/ui/ui.FadeText/-SPEC.tsx'), + 'tdb.slc.content.Image': () => import('../ui.Content/ui/ui.Image/-SPEC.tsx'), +} as t.SpecImports; diff --git a/deploy/@tdb.slc/src/-test/entry.tsx b/deploy/@tdb.slc/src/-test/entry.tsx new file mode 100644 index 0000000000..6c89e06973 --- /dev/null +++ b/deploy/@tdb.slc/src/-test/entry.tsx @@ -0,0 +1,69 @@ +import React, { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import type { t } from '../common.ts'; +import { pkg } from '../pkg.ts'; +import { useKeyboard } from '../ui/use/use.Keyboard.ts'; + +/** + * Render UI. + */ +const document = globalThis.document; +if (document) { + document.title = pkg.name; + document.body.style.overflow = 'hidden'; // NB: suppress rubber-band effect. +} + +/** + * Setup mounter: + */ +const Root = (props: { state: t.AppSignals; children?: t.ReactNode }) => { + useKeyboard(props.state); + return props.children; +}; + +/** + * MAIN entry. + */ +export async function main() { + const params = new URL(location.href).searchParams; + const isDev = params.has('dev') || params.has('d'); + const root = createRoot(document.getElementById('root')!); + + if (isDev) { + /** + * DevHarness: + */ + const { App } = await import('../ui/App/mod.ts'); + const { render } = await import('@sys/ui-react-devharness'); + const { Specs } = await import('./entry.Specs.ts'); + + const app = App.signals(); + const el = await render(pkg, Specs, { hrDepth: 3, style: { Absolute: 0 } }); + + root.render( + <StrictMode> + <Root state={app}>{el}</Root> + </StrictMode>, + ); + } else { + /** + * Landing (entry): + */ + const { Landing, App, Content } = await import('../ui/ui.Landing-3/mod.ts'); + const app = App.signals(); + + app.stack.push(await Content.Factory.entry()); + await App.Render.preload(app, Content.factory, 'Entry', 'Trailer', 'Overview'); + + root.render( + <StrictMode> + <Root state={app}> + <Landing state={app} style={{ Absolute: 0 }} /> + </Root> + </StrictMode>, + ); + } +} + +main().catch((err) => console.error(`Failed to render DevHarness`, err)); diff --git a/deploy/slc.db.team/src/-test/index.html b/deploy/@tdb.slc/src/-test/index.html similarity index 60% rename from deploy/slc.db.team/src/-test/index.html rename to deploy/@tdb.slc/src/-test/index.html index 1bca1020b7..b9285c2c69 100644 --- a/deploy/slc.db.team/src/-test/index.html +++ b/deploy/@tdb.slc/src/-test/index.html @@ -3,11 +3,6 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - - <meta name="apple-mobile-web-app-capable" content="yes" /> - <meta name="apple-mobile-web-app-status-bar-style" content="black" /> - <link rel="manifest" href="./manifest.json" /> - <title>loading...</title> </head> <body> diff --git a/deploy/@tdb.slc/src/-test/mod.ts b/deploy/@tdb.slc/src/-test/mod.ts new file mode 100644 index 0000000000..b58a897701 --- /dev/null +++ b/deploy/@tdb.slc/src/-test/mod.ts @@ -0,0 +1,2 @@ +export { c, describe, expect, expectError, it, Testing } from '@sys/testing/server'; +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/.test.ts b/deploy/@tdb.slc/src/.test.ts new file mode 100644 index 0000000000..7348c03d0a --- /dev/null +++ b/deploy/@tdb.slc/src/.test.ts @@ -0,0 +1,8 @@ +import { type t, describe, it, expect, Pkg, pkg } from './-test.ts'; + +describe(`module: ${Pkg.toString(pkg)}`, () => { + it('exists', () => { + console.info(`š¦ Module`, pkg); + expect(typeof pkg.name === 'string').to.be.true; + }); +}); diff --git a/deploy/slc.db.team/src/common.ts b/deploy/@tdb.slc/src/common.ts similarity index 100% rename from deploy/slc.db.team/src/common.ts rename to deploy/@tdb.slc/src/common.ts diff --git a/deploy/@tdb.slc/src/common/libs.ts b/deploy/@tdb.slc/src/common/libs.ts new file mode 100644 index 0000000000..140530b65c --- /dev/null +++ b/deploy/@tdb.slc/src/common/libs.ts @@ -0,0 +1 @@ +export { Err, Is, isRecord, Path, Pkg, rx, Signal, slug, Str, Time, Timestamp } from '@sys/std'; diff --git a/deploy/@tdb.slc/src/common/m.CanvasPanel.ts b/deploy/@tdb.slc/src/common/m.CanvasPanel.ts new file mode 100644 index 0000000000..6aecb6ce23 --- /dev/null +++ b/deploy/@tdb.slc/src/common/m.CanvasPanel.ts @@ -0,0 +1,19 @@ +import type * as t from './t.ts'; + +export const CanvasPanel = { + get list(): t.CanvasPanel[] { + return [ + 'purpose', + 'impact', + 'problem', + 'solution', + 'metrics', + 'uvp', + 'advantage', + 'channels', + 'customers', + 'costs', + 'revenue', + ]; + }, +} as const; diff --git a/deploy/@tdb.slc/src/common/mod.ts b/deploy/@tdb.slc/src/common/mod.ts new file mode 100644 index 0000000000..2ea69b26b5 --- /dev/null +++ b/deploy/@tdb.slc/src/common/mod.ts @@ -0,0 +1,7 @@ +import type * as t from './t.ts'; + +export { pkg } from '../pkg.ts'; +export { type t }; + +export * from './libs.ts'; +export * from './m.CanvasPanel.ts'; diff --git a/deploy/@tdb.slc/src/common/t.ts b/deploy/@tdb.slc/src/common/t.ts new file mode 100644 index 0000000000..bb53c40cdd --- /dev/null +++ b/deploy/@tdb.slc/src/common/t.ts @@ -0,0 +1,29 @@ +/** + * @external + */ +export type { ReactElement, ReactNode } from 'react'; + +/** + * @system + */ +export type * from '@sys/types'; + +export type { ExtractSignalValue, Signal } from '@sys/std/t'; +export type { SpecImports } from '@sys/testing/t'; +export type { CssEdgesInput, CssInput, CssMarginArray, CssProps, CssValue } from '@sys/ui-css/t'; +export type { + SheetMarginInput, + SheetOrientation, + SheetOrientationY, + SheetSignalStack, + SvgElement, + SvgInstance, + VideoPlayerSignals, + VimeoIFrame, +} from '@sys/ui-react-components/t'; +export type { DevCtx } from '@sys/ui-react-devharness/t'; + +/** + * @local + */ +export type * from '../types.ts'; diff --git a/deploy/@tdb.slc/src/mod.ts b/deploy/@tdb.slc/src/mod.ts new file mode 100644 index 0000000000..5246807c9a --- /dev/null +++ b/deploy/@tdb.slc/src/mod.ts @@ -0,0 +1,13 @@ +/** + * @module + * The "Social Lean Canvas" product system. + */ +export { pkg } from './pkg.ts'; + +/** Module types. */ +export type * as t from './types.ts'; + +/** + * Library + */ +export { App } from './ui/App/mod.ts'; diff --git a/deploy/@tdb.slc/src/pkg.ts b/deploy/@tdb.slc/src/pkg.ts new file mode 100644 index 0000000000..495d9f86eb --- /dev/null +++ b/deploy/@tdb.slc/src/pkg.ts @@ -0,0 +1,16 @@ +import type { Pkg } from '@sys/types'; + +/** + * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). + */ +export const pkg: Pkg = { name: '@tdb/slc', version: '0.0.1' }; diff --git a/deploy/@tdb.slc/src/types.ts b/deploy/@tdb.slc/src/types.ts new file mode 100644 index 0000000000..dcf4e0b048 --- /dev/null +++ b/deploy/@tdb.slc/src/types.ts @@ -0,0 +1,59 @@ +/** + * @module + * Module types. + */ + +/** + * UI: logical "App" state. + */ +export type * from './ui/App.Layout/t.ts'; +export type * from './ui/App.Render/t.ts'; +export type * from './ui/App.Signals.Controller/t.ts'; +export type * from './ui/App.Signals/t.ts'; +export type * from './ui/App/t.ts'; + +/** + * UI Structure: + */ +export type * from './ui/ui.Layout/t.ts'; +export type * from './ui/ui.Logo.Canvas/t.ts'; +export type * from './ui/ui.Logo.Wordmark/t.ts'; +export type * from './ui/ui.Sheet/t.ts'; +export type * from './ui/ui.TooSmall/t.ts'; +export type * from './ui/ui.Video.Background/t.ts'; + +export type * from './ui/ui.Landing-1/t.ts'; +export type * from './ui/ui.Landing-2/t.ts'; +export type * from './ui/ui.Landing-3/t.ts'; + +export type * from './ui/use/t.ts'; + +/** + * UI Content: + */ +export type * from './ui.Content/m.Content/t.ts'; +export type * from './ui.Content/m.Factory/t.ts'; +export type * from './ui.Content/t.ts'; +export type * from './ui.Content/ui/t.ts'; +export type * from './ui.Content/ui/ui.CanvasSlug/t.ts'; +export type * from './ui.Content/ui/ui.FadeText/t.ts'; +export type * from './ui.Content/ui/ui.Image/t.ts'; +export type * from './ui.Content/ui/ui.Pulldown/t.ts'; + +export type * from './ui.Content/-sample/ui.Videos/t.ts'; + +/** + * SLC Panels. + */ +export type CanvasPanel = + | 'purpose' + | 'impact' + | 'problem' + | 'solution' + | 'metrics' + | 'uvp' + | 'advantage' + | 'channels' + | 'customers' + | 'costs' + | 'revenue'; diff --git a/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/-SPEC.Debug.tsx new file mode 100644 index 0000000000..ef8a0c6856 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/-SPEC.Debug.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { type t, Button, Color, css, Player, Signal, VIDEO } from './common.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const video = Player.Video.signals({ + src: VIDEO.Trailer.src, // Rowan: "group scale", + cornerRadius: 0, + }); + const props = { + theme: s<t.CommonTheme>('Light'), + video, + }; + + const api = { + props, + listen() { + const p = props; + p.theme.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<P> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => { + p.theme.value; + p.video.props.src.value; + }); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + }; + + const selectVideo = (label: string, src: string) => { + const s = p.video.props; + const isCurrent = s.src.value === src; + const styles = { + base: css({ display: 'grid', gridTemplateColumns: `1fr auto`, marginLeft: 15 }), + label: css({ color: isCurrent ? Color.BLUE : undefined }), + }; + return ( + <Button block onClick={() => (s.src.value = src)}> + <div className={styles.base.class}> + <div className={styles.label.class}>{label}</div> + <div>{src}</div> + </div> + </Button> + ); + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`theme: "${p.theme}"`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + <div className={styles.title.class}>Videos</div> + {selectVideo('Trailer', VIDEO.Trailer.src)} + {selectVideo('Overview', VIDEO.Overview.src)} + <hr /> + {selectVideo('ref: "Group Scale"', VIDEO.GroupScale.src)} + {selectVideo('sample: Hindi Translation', 'vimeo/1074559094')} + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/-SPEC.tsx b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/-SPEC.tsx new file mode 100644 index 0000000000..de1aa8269e --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/-SPEC.tsx @@ -0,0 +1,28 @@ +import { Dev, Signal, Spec } from '../../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { VideosIndex } from './mod.ts'; + +export default Spec.describe('VideosIndex', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size([null, null]) + .display('grid') + .render((e) => <VideosIndex theme={p.theme.value} signals={p.video} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/common.ts b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/common.ts new file mode 100644 index 0000000000..aee2707a58 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/common.ts @@ -0,0 +1,7 @@ +export { VIDEO } from '../../VIDEO.ts'; +export * from '../../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/mod.ts b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/mod.ts new file mode 100644 index 0000000000..d2b68a3bf4 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { VideosIndex } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/t.ts b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/t.ts new file mode 100644 index 0000000000..42c7a97c09 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/t.ts @@ -0,0 +1,10 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type VideosIndexProps = { + signals?: t.VideoPlayerSignals; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/ui.tsx b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/ui.tsx new file mode 100644 index 0000000000..38aae9ad1e --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-sample/ui.Videos/ui.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { type t, Color, css, Player } from './common.ts'; + +export const VideosIndex: React.FC<t.VideosIndexProps> = (props) => { + const {} = props; + const signalsRef = React.useRef(props.signals); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + color: theme.fg, + width: 688, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Player.Video.View signals={signalsRef.current} /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/-test.ui.ts b/deploy/@tdb.slc/src/ui.Content/-test.ui.ts new file mode 100644 index 0000000000..80e99d3a9c --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/-test.ui.ts @@ -0,0 +1 @@ +export * from '../ui/-test.ui.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/VIDEO.ts b/deploy/@tdb.slc/src/ui.Content/VIDEO.ts new file mode 100644 index 0000000000..d50a5a1989 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/VIDEO.ts @@ -0,0 +1,11 @@ +import { vimeo, TUBES } from './common.ts'; + +/** + * Index of Video IDs. + */ +export const VIDEO = { + Tubes: TUBES, + GroupScale: vimeo(727951677), + Trailer: vimeo(1068502644), + Overview: vimeo(1068653222), +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/common.ts b/deploy/@tdb.slc/src/ui.Content/common.ts new file mode 100644 index 0000000000..45b6d3a168 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/common.ts @@ -0,0 +1 @@ +export * from './common/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/common/libs.ts b/deploy/@tdb.slc/src/ui.Content/common/libs.ts new file mode 100644 index 0000000000..256ffc6f31 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/common/libs.ts @@ -0,0 +1,3 @@ +export { DEFAULTS } from '../../ui/App.Render/mod.ts'; +export { App, LogoCanvas, LogoWordmark, Sheet } from '../../ui/mod.ts'; +export { TooSmall } from '../../ui/ui.TooSmall/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/common/mod.ts b/deploy/@tdb.slc/src/ui.Content/common/mod.ts new file mode 100644 index 0000000000..5a921e0ef1 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/common/mod.ts @@ -0,0 +1,14 @@ +export * from '../../ui/common.ts'; +export { Icons } from '../ui.Icons.ts'; + +export * from './libs.ts'; +export type * as t from './t.ts'; + +/** + * A curried image importer helpers. + */ +export function i(importer: () => Promise<any>) { + return async () => { + return (await importer()).default as string; + }; +} diff --git a/deploy/@tdb.slc/src/ui.Content/common/t.ts b/deploy/@tdb.slc/src/ui.Content/common/t.ts new file mode 100644 index 0000000000..1e20daf06b --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/common/t.ts @@ -0,0 +1 @@ +export type * from '../../common/t.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Content/-.test.ts b/deploy/@tdb.slc/src/ui.Content/m.Content/-.test.ts new file mode 100644 index 0000000000..4b981d294e --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Content/-.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from '../../-test.ts'; +import { Factory, factory } from '../m.Factory/mod.ts'; +import { Content } from './mod.ts'; + +describe('Content', () => { + it('API', () => { + expect(Content.Factory).to.equal(Factory); + expect(Content.factory).to.equal(factory); + }); + + describe('Content.Is', () => { + const Is = Content.Is; + const NON = ['', 123, true, null, undefined, BigInt(0), Symbol('foo'), {}, []]; + + it('Is.video', async () => { + const test = (input: any, expected: boolean) => expect(Is.video(input)).to.eql(expected); + test(await factory('Trailer'), true); + test(await factory('Overview'), true); + NON.forEach((value) => test(value, false)); + }); + + it('Is.static', async () => { + const test = (input: any, expected: boolean) => expect(Is.static(input)).to.eql(expected); + test(await factory('Entry'), true); + test(await factory('Programme'), true); + NON.forEach((value) => test(value, false)); + }); + }); +}); diff --git a/deploy/@tdb.slc/src/ui.Content/m.Content/common.ts b/deploy/@tdb.slc/src/ui.Content/m.Content/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Content/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Content/m.Content.ts b/deploy/@tdb.slc/src/ui.Content/m.Content/m.Content.ts new file mode 100644 index 0000000000..e3b54a439f --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Content/m.Content.ts @@ -0,0 +1,14 @@ +/** + * @module + * Content library + */ +import type { t } from './common.ts'; + +import { Factory, factory } from '../m.Factory/mod.ts'; +import { Is } from './m.Is.ts'; + +export const Content: t.ContentLib = { + Is, + Factory, + factory, +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Content/m.Is.ts b/deploy/@tdb.slc/src/ui.Content/m.Content/m.Is.ts new file mode 100644 index 0000000000..5defc28f4a --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Content/m.Is.ts @@ -0,0 +1,15 @@ +/** + * @module + * Content library + */ +import { type t, isRecord } from './common.ts'; + +export const Is: t.ContentIs = { + video(input): input is t.VideoContent { + return isRecord(input) && (input as t.VideoContent)['-type'] === 'VideoContent'; + }, + + static(input): input is t.StaticContent { + return isRecord(input) && (input as t.StaticContent)['-type'] === 'StaticContent'; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Content/mod.ts b/deploy/@tdb.slc/src/ui.Content/m.Content/mod.ts new file mode 100644 index 0000000000..25a26e0dcb --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Content/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Content library. + */ +export { Content } from './m.Content.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Content/t.ts b/deploy/@tdb.slc/src/ui.Content/m.Content/t.ts new file mode 100644 index 0000000000..f944474a4d --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Content/t.ts @@ -0,0 +1,18 @@ +import type { t } from './common.ts'; + +/** + * Content library API. + */ +export type ContentLib = { + readonly Is: t.ContentIs; + readonly Factory: t.ContentFactoryLib; + readonly factory: t.ContentFactory; +}; + +/** + * Content flags. + */ +export type ContentIs = { + video(input: any): input is t.VideoContent; + static(input: any): input is t.StaticContent; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Factory/-.test.ts b/deploy/@tdb.slc/src/ui.Content/m.Factory/-.test.ts new file mode 100644 index 0000000000..821b816853 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Factory/-.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from '../../-test.ts'; +import { Content } from '../mod.ts'; +import { VIDEO } from '../VIDEO.ts'; + +describe('Content.Factory', () => { + it('Entry', async () => { + const a = await Content.Factory.entry(); + const b = await Content.factory('Entry'); + expect(a?.id).to.eql('Entry'); + expect(b?.id).to.equal(a?.id); + }); + + it('Trailer', async () => { + const a = await Content.Factory.trailer(); + const b = await Content.factory('Trailer'); + expect(a?.id).to.eql('Trailer'); + expect(b?.id).to.equal(a?.id); + expect(a?.video?.props.src.value).to.eql(VIDEO.Trailer.src); + }); + + it('Overview', async () => { + const a = await Content.Factory.overview(); + const b = await Content.factory('Overview'); + expect(a?.id).to.eql('Overview'); + expect(b?.id).to.equal(a?.id); + expect(a?.video?.props.src.value).to.eql(VIDEO.Overview.src); + }); + + it('Programme', async () => { + const a = await Content.Factory.programme(); + const b = await Content.factory('Programme'); + expect(a?.id).to.eql('Programme'); + expect(b?.id).to.equal(a?.id); + }); +}); diff --git a/deploy/@tdb.slc/src/ui.Content/m.Factory/m.Factory.tsx b/deploy/@tdb.slc/src/ui.Content/m.Factory/m.Factory.tsx new file mode 100644 index 0000000000..ef793acdcf --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Factory/m.Factory.tsx @@ -0,0 +1,22 @@ +import type { t } from '../common.ts'; + +export const Factory: t.ContentFactoryLib = { + entry: async () => (await import('../ui.Entry/mod.ts')).factory(), + trailer: async () => (await import('../ui.Trailer/mod.ts')).factory(), + overview: async () => (await import('../ui.Overview/mod.ts')).factory(), + programme: async () => (await import('../ui.Programme/mod.ts')).factory(), +} as const; + +/** + * Look up and dynamically import the content for the given ID. + */ +export const factory: t.ContentFactory = async (id) => { + if (id === 'Entry') return Factory.entry(); + if (id === 'Trailer') return Factory.trailer(); + if (id === 'Overview') return Factory.overview(); + if (id === 'Programme') return Factory.programme(); + + // Not found. + console.warn(`Content with id "${id}" not found.`); + return undefined; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Factory/mod.ts b/deploy/@tdb.slc/src/ui.Content/m.Factory/mod.ts new file mode 100644 index 0000000000..2d3abcaf69 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Factory/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Factories for dynamic ESM content imports. + */ +export { factory, Factory } from './m.Factory.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/m.Factory/t.ts b/deploy/@tdb.slc/src/ui.Content/m.Factory/t.ts new file mode 100644 index 0000000000..a77a3f4d08 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/m.Factory/t.ts @@ -0,0 +1,16 @@ +import type { t } from '../common.ts'; + +/** + * Content factory library. + */ +export type ContentFactoryLib = { + entry(): Promise<t.StaticContent>; + trailer(): Promise<t.VideoContent>; + overview(): Promise<t.VideoContent>; + programme(): Promise<t.StaticContent>; +}; + +/** + * Content factory. + */ +export type ContentFactory = (id: t.ContentStage) => Promise<t.Content | undefined>; diff --git a/deploy/@tdb.slc/src/ui.Content/mod.ts b/deploy/@tdb.slc/src/ui.Content/mod.ts new file mode 100644 index 0000000000..f75d47ea05 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Content entry point. + */ +export { Content } from './m.Content/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/t.ts b/deploy/@tdb.slc/src/ui.Content/t.ts new file mode 100644 index 0000000000..480b327115 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/t.ts @@ -0,0 +1,39 @@ +import type { t } from './common.ts'; + +/** + * The content stages of the view. + */ +export type ContentStage = 'Entry' | 'Trailer' | 'Overview' | 'Programme'; + +/** + * Time based content definition. + */ +export type ContentTimestamps = t.Timestamps<ContentTimestamp>; +export type ContentTimestamp = ContentTimestampProps | ContentTimestampProps['column']; +export type ContentTimestampProps = { + column?: t.VideoContentRenderer; + pulldown?: t.VideoContentRenderer; +}; + +/** + * Content variation: Video. + */ +export type VideoContent = t.Content<V & { '-type': 'VideoContent' }>; +export type VideoContentProps = t.ContentProps<V>; +type V = { + id: t.ContentStage; + timestamps: ContentTimestamps; + showElapsed?: boolean; + playOnLoad?: boolean; + video?: t.VideoPlayerSignals; +}; + +export type VideoContentRenderer = t.ContentRenderer<t.VideoContentProps>; + +/** + * Content variation: Static. + */ +export type StaticContent = t.Content<{ + '-type': 'StaticContent'; + id: t.ContentStage; +}>; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/common.ts b/deploy/@tdb.slc/src/ui.Content/ui.Entry/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/m.Factory.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Entry/m.Factory.tsx new file mode 100644 index 0000000000..eb82ddcbe5 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/m.Factory.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { type t, DEFAULTS } from './common.ts'; +import { Entry } from './ui.tsx'; + +export function factory() { + const content: t.StaticContent = { + '-type': 'StaticContent', + id: 'Entry', + render: (props) => <Entry {...props} theme={DEFAULTS.theme.base} />, + }; + return content; +} diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui.Entry/mod.ts new file mode 100644 index 0000000000..048c873c21 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/mod.ts @@ -0,0 +1 @@ +export * from './m.Factory.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Button.Rounded.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Button.Rounded.tsx new file mode 100644 index 0000000000..bfc4218d28 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Button.Rounded.tsx @@ -0,0 +1,97 @@ +import React, { useEffect, useState } from 'react'; +import { type t, Button, Color, css, Time } from './common.ts'; + +export type RoundedButtonProps = { + label?: string; + pulse?: boolean | Pulse; + theme?: t.CommonTheme; + style?: t.CssInput; + onClick?: MouseHandler; +}; + +export type Pulse = { pulsing?: boolean; duration?: t.Msecs; opacity?: t.Percent }; + +type P = RoundedButtonProps; +type MouseHandler = React.MouseEventHandler; + +/** + * Component: + */ +export const RoundedButton: React.FC<P> = (props) => { + const { label = 'Unnamed' } = props; + const pulse = wrangle.pulse(props.pulse); + + const [isOver, setOver] = useState(false); + const [pulseOpacity, setPulseOpacity] = useState(0); // New state for opacity pulsing + + /** + * Effects: + * When pulse is active (pulsing is true), set up a cycle that toggles the pulseOpacity value. + */ + useEffect(() => { + if (!pulse.pulsing) { + setPulseOpacity(0); + return; + } + + const time = Time.until(); + const duration = pulse.duration; + const toggleOpacity = () => { + setPulseOpacity((prev) => (prev === 0 ? 1 : 0)); + time.delay(duration, toggleOpacity); + }; + + toggleOpacity(); + return time.dispose; + }, [pulse.duration, pulse.pulsing]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ position: 'relative', color: theme.fg }), + body: { + base: css({ + position: 'relative', + minWidth: 70, + backgroundColor: Color.alpha(theme.fg, isOver ? 1 : 0.15), + transition: `background-color 100ms ease-in-out`, + Padding: [12, 25], + borderRadius: 40, + border: `solid 1px ${Color.alpha(theme.fg, 0.8)}`, + display: 'grid', + }), + content: css({ display: 'grid', placeItems: 'center' }), + pulse: css({ + Absolute: 0, + Padding: [12, 25], + borderRadius: 40, + backgroundColor: Color.alpha(theme.fg, pulse.opacity), + opacity: pulse ? pulseOpacity : 1, + transition: `opacity ${pulse.duration}ms ease-in-out`, + }), + }, + }; + + return ( + <Button theme={theme.name} onClick={props.onClick} onMouse={(e) => setOver(e.isOver)}> + <div className={styles.body.base.class}> + <div className={styles.body.pulse.class} /> + <div className={styles.body.content.class}>{label}</div> + </div> + </Button> + ); +}; + +/** + * Helpers + */ +const wrangle = { + pulse(input?: P['pulse']): Required<Pulse> { + const DEFAULT: Required<Pulse> = { duration: 1500, pulsing: false, opacity: 0.3 }; + if (!input) return DEFAULT; + if (input === true) return { ...DEFAULT, pulsing: true }; + return { ...DEFAULT, ...input }; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Buttons.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Buttons.tsx new file mode 100644 index 0000000000..4ef0fec586 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Buttons.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Factory } from '../m.Factory/mod.ts'; +import { type t, Color, css } from './common.ts'; +import { RoundedButton } from './ui.Button.Rounded.tsx'; + +export type ButtonsProps = { + state: t.AppSignals; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +/** + * Component: + */ +export const Buttons: React.FC<ButtonsProps> = (props) => { + const { state } = props; + + /** + * Handlers: + */ + const stack = state.stack; + const showTrailer = async () => stack.push(await Factory.trailer()); + const showOverview = async () => stack.push(await Factory.overview()); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + color: theme.fg, + display: 'grid', + gridTemplateColumns: `auto auto`, + columnGap: '15px', + }), + button: css({ + Padding: [15, 40], + borderRadius: 40, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <RoundedButton theme={theme.name} label={'Trailer'} onClick={showTrailer} pulse /> + <RoundedButton theme={theme.name} label={'Overview'} onClick={showOverview} /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Install.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Install.tsx new file mode 100644 index 0000000000..781af19776 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.Install.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { type t, css, Icons } from './common.ts'; + +export type InstallProps = { + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +/** + * Component: + */ +export const Install: React.FC<InstallProps> = (props) => { + const {} = props; + + /** + * Render: + */ + const styles = { + base: css({ + position: 'relative', + userSelect: 'none', + display: 'grid', + justifyItems: 'center', + }), + label: css({ opacity: 0.2 }), + icon: css({ Size: 44, display: 'grid', placeItems: 'center' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.label.class}>{`Install ( SLC System )`}</div> + <div className={styles.icon.class}> + <Icons.Arrow.Down /> + </div> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.tsx new file mode 100644 index 0000000000..0a02835d6c --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Entry/ui.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { type t, Color, css, Icons, LogoCanvas, LogoWordmark } from './common.ts'; +import { Buttons } from './ui.Buttons.tsx'; +import { Install } from './ui.Install.tsx'; + +export type EntryProps = t.ContentProps & {}; + +const heartRateBPM = 72; +const delay = 60_000 / heartRateBPM; // NB: 60_000 ms in a minute. + +/** + * Component: + */ +export const Entry: React.FC<EntryProps> = (props) => { + const { state } = props; + const breakpoint = state.breakpoint; + const isStandalone = window.matchMedia('(display-mode: standalone)').matches; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + color: theme.fg, + pointerEvents: 'auto', + display: 'grid', + gridTemplateRows: '44px 1fr auto', + }), + header: css({ + MarginX: 10, + borderBottom: `solid 1px ${Color.alpha(theme.fg, 0.1)}`, + display: 'grid', + placeItems: 'center', + }), + body: css({ display: 'grid', placeItems: 'center' }), + footer: css({ display: breakpoint.name === 'Mobile' ? 'grid' : 'none' }), + brand: { + base: css({ display: 'grid', placeItems: 'center', rowGap: '35px' }), + canvas: css({ MarginX: 70 }), + wordmark: css({ width: 120 }), + }, + }; + + return ( + <div className={css(styles.base, props.style).class} onClick={() => state.stack.clear(1)}> + <div className={styles.header.class}> + <Icons.Add.Plus opacity={0.2} /> + </div> + <div className={styles.body.class}> + <div className={styles.brand.base.class}> + <LogoCanvas + theme={theme.name} + style={styles.brand.canvas} + selected={'purpose'} + selectionAnimation={{ delay, loop: true }} + /> + <LogoWordmark theme={theme.name} style={styles.brand.wordmark} /> + <Buttons theme={theme.name} state={state} style={{ marginTop: 100 }} /> + </div> + </div> + <div className={styles.footer.class}>{!isStandalone && <Install theme={theme.name} />}</div> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Icons.ts b/deploy/@tdb.slc/src/ui.Content/ui.Icons.ts new file mode 100644 index 0000000000..75fbee79a4 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Icons.ts @@ -0,0 +1,10 @@ +import { MdFace } from 'react-icons/md'; +import { Icons as Base, icon } from '../ui/ui.Icons.ts'; + +/** + * Icon collection: + */ +export const Icons = { + ...Base, + Face: icon(MdFace), +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/common.ts b/deploy/@tdb.slc/src/ui.Content/ui.Overview/common.ts new file mode 100644 index 0000000000..41de695810 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Overview/common.ts @@ -0,0 +1,2 @@ +export * from '../common.ts'; +export { CanvasSlug, Image } from '../ui/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/build-measure-learn.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/build-measure-learn.png new file mode 100644 index 0000000000..f618f185ea Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/build-measure-learn.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/customer-model.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/customer-model.png new file mode 100644 index 0000000000..e995d1c768 Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/customer-model.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/defining-purpose.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/defining-purpose.png new file mode 100644 index 0000000000..6cb4ec8f0b Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/defining-purpose.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/economic-model.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/economic-model.png new file mode 100644 index 0000000000..8db203df07 Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/economic-model.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/failure.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/failure.png new file mode 100644 index 0000000000..6977a3e11b Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/failure.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/impact-model.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/impact-model.png new file mode 100644 index 0000000000..fdfdcd6e1e Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/impact-model.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/model-parts.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/model-parts.png new file mode 100644 index 0000000000..8779bf87e3 Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/model-parts.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/refine.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/refine.png new file mode 100644 index 0000000000..034cb75a48 Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/refine.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/strategy.png b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/strategy.png new file mode 100644 index 0000000000..fae31a0406 Binary files /dev/null and b/deploy/@tdb.slc/src/ui.Content/ui.Overview/img/strategy.png differ diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/m.Factory.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Overview/m.Factory.tsx new file mode 100644 index 0000000000..9186166fc9 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Overview/m.Factory.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { VIDEO } from '../VIDEO.ts'; + +import { type t, DEFAULTS, Player } from './common.ts'; +import { timestamps } from './u.timestamps.tsx'; +import { Overview } from './ui.tsx'; + +/** + * Content: "Overview" (2 minute summary). + */ +export function factory() { + const theme = DEFAULTS.theme.sheet; + const src = VIDEO.Overview.src; + + const content: t.VideoContent = { + '-type': 'VideoContent', + + id: 'Overview', + playOnLoad: true, + video: Player.Video.signals({ + src, + scale: (e) => e.enlargeBy(2), // NB: enlarge 2px to crop out noise/line at top of video. + }), + render: (props) => <Overview {...props} theme={theme} />, + timestamps, + }; + return content; +} diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/m.Images.ts b/deploy/@tdb.slc/src/ui.Content/ui.Overview/m.Images.ts new file mode 100644 index 0000000000..6018194cf6 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Overview/m.Images.ts @@ -0,0 +1,13 @@ +import { i } from './common.ts'; + +export const Images = { + build: i(() => import('./img/build-measure-learn.png')), + impactModel: i(() => import('./img/impact-model.png')), + customerModel: i(() => import('./img/customer-model.png')), + modelParts: i(() => import('./img/model-parts.png')), + definingPurpose: i(() => import('./img/defining-purpose.png')), + refine: i(() => import('./img/refine.png')), + economicModel: i(() => import('./img/economic-model.png')), + strategy: i(() => import('./img/strategy.png')), + failure: i(() => import('./img/failure.png')), +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui.Overview/mod.ts new file mode 100644 index 0000000000..6192e59e17 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Overview/mod.ts @@ -0,0 +1,2 @@ +export * from './m.Factory.tsx'; +export * from './m.Images.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/u.timestamps.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Overview/u.timestamps.tsx new file mode 100644 index 0000000000..e3bebd5cbe --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Overview/u.timestamps.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { type t, CanvasSlug, Image } from './common.ts'; +import { Images } from './m.Images.ts'; + +const Slug = CanvasSlug; + +/** + * Overview: + */ +export const timestamps: t.ContentTimestamps = { + '00:00:00.000': (p) => <Slug {...p} logo={'SLC'} />, + '00:00:01.000': (p) => <Slug {...p} />, + '00:00:02.300': (p) => <Slug {...p} text={'slow'} />, + '00:00:03.210': (p) => <Slug {...p} text={'risky'} />, + '00:00:04.300': (p) => <Slug {...p} text={'mostly\nunsuccessful'} />, + '00:00:06.350': (p) => <Slug {...p} />, + '00:00:08.000': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.failure()} />, + }, + '00:00:16.000': (p) => <Slug {...p} />, + '00:00:16.850': (p) => <Slug {...p} text={'business model'} />, + '00:00:18.580': (p) => <Slug {...p} text={'economic\nfoundation'} />, + '00:00:23.550': (p) => <Slug {...p} text={'lasting\nmeasurable\nimpact'} selected={'impact'} />, + '00:00:36.000': (p) => <Slug {...p} />, + '00:00:36.250': (p) => <Slug {...p} text={'fast fail'} />, + '00:00:42.000': (p) => <Slug {...p} text={'living dead'} />, + '00:00:53.000': (p) => <Slug {...p} />, + '00:00:58.300': (p) => <Slug {...p} text={'shared\nsense-making'} />, + '00:01:04.000': (p) => <Slug {...p} logo={'SLC'} />, + '00:01:09.000': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.build()} padding={'5%'} />, + }, + '00:01:23.000': (p) => <Slug {...p} logo={'SLC'} />, + '00:01:25.250': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.definingPurpose()} />, + }, + '00:01:37.900': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.modelParts()} padding={'10%'} />, + }, + '00:01:49.000': (p) => <Slug {...p} logo={'SLC'} />, + '00:01:50.810': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.customerModel()} />, + }, + '00:02:11.000': (p) => <Slug {...p} text={'simple'} />, + '00:02:16.000': (p) => <Slug {...p} text={'complex'} />, + '00:02:23.360': (p) => <Slug {...p} text={'complex\nprotocol'} />, + '00:02:33.460': (p) => <Slug {...p} />, + '00:02:37.000': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.impactModel()} />, + }, + '00:02:43.660': (p) => <Slug {...p} text={'proveable\nimpact'} />, + '00:02:48.000': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.economicModel()} />, + }, + '00:03:04.660': (p) => <Slug {...p} logo={'SLC'} />, + '00:03:10.000': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.refine()} />, + }, + '00:03:15.730': (p) => <Slug {...p} text={'genuinely\nhigh\npotential'} />, + '00:03:19.250': { + column: (p) => <Slug {...p} logo={'SLC'} />, + pulldown: (p) => <Image.View src={Images.strategy()} />, + }, + '00:03:33.000': (p) => <Slug {...p} logo={'CC'} />, +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Overview/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Overview/ui.tsx new file mode 100644 index 0000000000..e6d4f38dfa --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Overview/ui.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { ElapsedTime, usePulldown, useTimestamps } from '../ui/mod.ts'; +import { type t, css, Player, Sheet, Time } from './common.ts'; + +export type OverviewProps = t.VideoContentProps; + +/** + * Component: + */ +export const Overview: React.FC<OverviewProps> = (props) => { + const { state, content } = props; + const { showElapsed = true } = content; + + const player = content.video; + const timestamp = useTimestamps(props, player); + usePulldown(props, timestamp); + + /** + * Effect: Play on load. + */ + React.useEffect(() => { + if (content.playOnLoad) player?.play(); + }, [player]); + + /** + * Render: + */ + const edge: t.SheetMarginInput = state.breakpoint.name === 'Desktop' ? ['1fr', 390, '1fr'] : 10; + const styles = { + base: css({ marginTop: 44 }), + body: css({ position: 'relative', display: 'grid', gridTemplateRows: '1fr auto' }), + content: css({ display: 'grid' }), + player: css({ marginBottom: -1 }), + }; + + const elBody = ( + <div className={styles.body.class}> + <div className={styles.content.class}>{timestamp.column}</div> + <Player.Video.View + signals={player} + style={styles.player} + onEnded={() => Time.delay(1000, () => state.stack.clear(1))} // NB: add time buffer before hiding. + /> + </div> + ); + + return ( + <Sheet + {...props} + style={styles.base} + theme={props.theme} + edgeMargin={edge} + orientation={'Bottom:Up'} + > + {elBody} + <ElapsedTime player={player} abs={true} show={showElapsed} /> + </Sheet> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Programme/m.Factory.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Programme/m.Factory.tsx new file mode 100644 index 0000000000..f6633ee069 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Programme/m.Factory.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { type t, Color, css, Sheet, App, Button, DEFAULTS } from '../common.ts'; + +/** + * Content: "Programme" + */ +export function factory() { + const sheetTheme = DEFAULTS.theme.sheet; + const content: t.StaticContent = { + '-type': 'StaticContent', + id: 'Programme', + + render(props) { + const styles = { + base: css({ padding: 10 }), + }; + + return ( + <Sheet {...props} theme={sheetTheme} orientation={'Top:Down'}> + <div className={styles.base.class}>Hello Programme</div> + {/* {props.children} */} + </Sheet> + ); + }, + }; + return content; +} diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Programme/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui.Programme/mod.ts new file mode 100644 index 0000000000..048c873c21 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Programme/mod.ts @@ -0,0 +1 @@ +export * from './m.Factory.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Trailer/common.ts b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/common.ts new file mode 100644 index 0000000000..370c668bd6 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/common.ts @@ -0,0 +1,2 @@ +export * from '../common.ts'; +export { CanvasSlug } from '../ui/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Trailer/m.Factory.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/m.Factory.tsx new file mode 100644 index 0000000000..607b76746d --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/m.Factory.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { VIDEO } from '../VIDEO.ts'; +import { type t, DEFAULTS, Player } from './common.ts'; +import { timestamps } from './u.timestamps.tsx'; +import { Trailer } from './ui.tsx'; + +/** + * Content: "Trailer" (30 second intro). + */ +export function factory() { + const theme = DEFAULTS.theme.sheet; + const src = VIDEO.Trailer.src; + + const content: t.VideoContent = { + '-type': 'VideoContent', + id: 'Trailer', + + playOnLoad: true, + video: Player.Video.signals({ + src, + scale: (e) => e.enlargeBy(2), // NB: enlarge 2px to crop out noise/line at top of video. + }), + + render: (props) => <Trailer {...props} theme={theme} />, + timestamps, + }; + + return content; +} diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Trailer/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/mod.ts new file mode 100644 index 0000000000..048c873c21 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/mod.ts @@ -0,0 +1 @@ +export * from './m.Factory.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Trailer/u.timestamps.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/u.timestamps.tsx new file mode 100644 index 0000000000..d93224b8d5 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/u.timestamps.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { type t, CanvasPanel, CanvasSlug } from './common.ts'; + +const Slug = CanvasSlug; +const panels = CanvasPanel.list; + +/** + * Trailer: + */ +export const timestamps: t.ContentTimestamps = { + '00:00:00.000': (p) => <Slug {...p} logo={'SLC'} />, + '00:00:00.001': (p) => <Slug {...p} text={'social ventures'} />, + '00:00:03.560': (p) => <Slug {...p} text={'good ideas'} />, + '00:00:07.000': (p) => <Slug {...p} text={'wrong priorities'} />, + + '00:00:11.870': (p) => <Slug {...p} selected={'purpose'} text={'purpose'} />, + '00:00:19.600': (p) => <Slug {...p} selected={panels} text={'decompose'} />, + '00:00:23.500': (p) => <Slug {...p} selected={panels.toReversed()} text={'recompose'} />, + '00:00:29.540': (p) => <Slug {...p} selected={'purpose'} logo={'SLC'} />, + '00:00:34.000': (p) => <Slug {...p} selected={'purpose'} text={'coherence'} />, + '00:00:37.590': (p) => <Slug {...p} selected={'purpose'} logo={'SLC'} />, + '00:00:47.350': (p) => <Slug {...p} selected={'purpose'} text={'shared clarity'} />, + '00:00:55.620': (p) => <Slug {...p} selected={'purpose'} logo={'CC'} />, +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui.Trailer/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/ui.tsx new file mode 100644 index 0000000000..3b997867dc --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui.Trailer/ui.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { css, Player, Sheet, type t, Time } from '../common.ts'; +import { ElapsedTime, useTimestamps } from '../ui/mod.ts'; + +export type TrailerProps = t.VideoContentProps; + +/** + * Component: + */ +export const Trailer: React.FC<TrailerProps> = (props) => { + const { state, content } = props; + const { showElapsed = true } = content; + const player = content.video; + const timestamp = useTimestamps(props, player); + + /** + * Effect: Play on load. + */ + React.useEffect(() => { + if (content.playOnLoad) player?.play(); + }, [player]); + + /** + * Render: + */ + const edge: t.SheetMarginInput = state.breakpoint.name === 'Desktop' ? ['1fr', 390, '1fr'] : 10; + const styles = { + base: css({ marginTop: 44 }), + body: css({ position: 'relative', display: 'grid', gridTemplateRows: '1fr auto' }), + content: css({ display: 'grid' }), + player: css({ marginBottom: -1 }), + }; + + return ( + <Sheet + {...props} + theme={props.theme} + style={styles.base} + edgeMargin={edge} + orientation={'Bottom:Up'} + > + <div className={styles.body.class}> + <div className={styles.content.class}>{timestamp.column}</div> + <Player.Video.View + signals={player} + style={styles.player} + onEnded={() => Time.delay(1000, () => state.stack.clear(1))} // NB: add time buffer before hiding. + /> + </div> + <ElapsedTime player={player} abs={true} show={showElapsed} /> + </Sheet> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/common.ts b/deploy/@tdb.slc/src/ui.Content/ui/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui/mod.ts new file mode 100644 index 0000000000..e6db1acb87 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/mod.ts @@ -0,0 +1,6 @@ +export * from './ui.CanvasSlug/mod.ts'; +export * from './ui.ElapsedTime.tsx'; +export * from './ui.Image/mod.ts'; +export * from './ui.Pulldown/mod.ts'; + +export * from './use.Timestamps.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/t.ts b/deploy/@tdb.slc/src/ui.Content/ui/t.ts new file mode 100644 index 0000000000..74bb1be51e --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/t.ts @@ -0,0 +1,14 @@ +import { type t } from './common.ts'; + +/** + * Hook for managing content. + */ +export type UseTimestamps = ( + props: t.VideoContentProps, + player?: t.VideoPlayerSignals, +) => TimestampsHook; + +export type TimestampsHook = { + readonly column?: t.ReactNode; + readonly pulldown?: t.ReactNode; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/-SPEC.Debug.tsx new file mode 100644 index 0000000000..003a03f433 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/-SPEC.Debug.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { canvasSelectedButton } from '../../../ui/ui.Logo.Canvas/-SPEC.Debug.tsx'; +import { type t, Button, css, D, Signal } from './common.ts'; + +type P = t.CanvasSlugProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + debug: s<P['debug']>(true), + theme: s<P['theme']>('Light'), + selected: s<P['selected']>(), + logo: s<P['logo']>(D.logo), + text: s<P['text']>(), + }; + const api = { + props, + listen() { + const p = props; + p.debug.value; + p.theme.value; + p.selected.value; + p.logo.value; + p.text.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}>{'CanvasSlug'}</div> + + <Button block label={() => `debug: ${p.debug}`} onClick={() => Signal.toggle(p.debug)} /> + <Button + block + label={() => `theme: ${p.theme}`} + onClick={() => Signal.cycle<P['theme']>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + <Button + block + label={() => `logo: "${p.logo}"`} + onClick={() => Signal.cycle<P['logo']>(p.logo, [undefined, 'SLC', 'CC'])} + /> + <Button + block + label={() => `text: ${p.text.value ?? '<undefined>'}`} + onClick={() => { + Signal.cycle<P['text']>(p.text, [undefined, 'hello', 'purpose\nimpact']); + }} + /> + {canvasSelectedButton(p.selected)} + + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/-SPEC.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/-SPEC.tsx new file mode 100644 index 0000000000..f54e8837f4 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/-SPEC.tsx @@ -0,0 +1,37 @@ +import { Dev, Signal, Spec } from '../../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { CanvasSlug } from './mod.ts'; + +export default Spec.describe('CanvasSlug', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size('fill-y', 150) + .display('grid') + .render((e) => ( + <CanvasSlug + style={{ width: 390 }} + debug={p.debug.value} + theme={p.theme.value} + selected={p.selected.value} + logo={p.logo.value} + text={p.text.value} + /> + )); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/common.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/common.ts new file mode 100644 index 0000000000..24d7deccd0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/common.ts @@ -0,0 +1,12 @@ +import type { t } from './common.ts'; +export * from '../common.ts'; + +/** + * Constants: + */ +const logo: t.LogoKind = 'SLC'; + +export const DEFAULTS = { + logo, +} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/mod.ts new file mode 100644 index 0000000000..1a70bbc6fe --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { CanvasSlug } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/t.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/t.ts new file mode 100644 index 0000000000..c183a8a3ef --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/t.ts @@ -0,0 +1,13 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type CanvasSlugProps = { + selected?: t.CanvasPanel | t.CanvasPanel[]; + logo?: t.LogoKind; + text?: string; + debug?: boolean; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/ui.tsx new file mode 100644 index 0000000000..2a0a39f9d3 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.CanvasSlug/ui.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { + type t, + Color, + css, + LogoCanvas, + LogoWordmark, + Time, + TooSmall, + useSizeObserver, + ReactString, +} from './common.ts'; +import { FadeText } from '../ui.FadeText/mod.ts'; + +export const CanvasSlug: React.FC<t.CanvasSlugProps> = (props) => { + const { debug = false, logo } = props; + const size = useSizeObserver(); + + const [ready, setReady] = useState(false); + + /** + * Effect: + */ + React.useEffect(() => { + const time = Time.until(); + time.delay(500, () => setReady(true)); + return time.dispose; + }, [size.ready]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + position: 'relative', + display: 'grid', + opacity: ready ? 1 : 0, + transition: '1200ms', + }), + body: css({ position: 'relative', display: 'grid', placeItems: 'center' }), + layout: css({ display: 'grid', placeItems: 'center', rowGap: '30px' }), + canvas: css({ position: 'relative', width: 280 }), + logo: css({ width: logo === 'SLC' ? 130 : 200 }), + footer: css({ + position: 'relative', + height: 115, + width: '100%', + display: 'grid', + placeItems: 'center', + }), + text: css({ Absolute: 0 }), + }; + + const elText = <FadeText text={logo ? '' : props.text} style={styles.text} />; + const elWordmark = logo && <LogoWordmark theme={theme.name} logo={logo} style={styles.logo} />; + const elFooter = ( + <div className={styles.footer.class}> + {elText} + {elWordmark} + </div> + ); + + const elBody = ( + <div className={styles.body.class}> + <div className={styles.layout.class}> + <LogoCanvas theme={theme.name} style={styles.canvas} selected={props.selected} /> + {elFooter} + </div> + </div> + ); + + const elTooSmall = size.ready && size.height < 320 && <TooSmall />; + + return ( + <div ref={size.ref} className={css(styles.base, props.style).class}> + {elTooSmall || elBody} + {debug && size.toElement({ Absolute: [6, 8, null, null], opacity: 0.3 })} + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.ElapsedTime.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.ElapsedTime.tsx new file mode 100644 index 0000000000..68e058f49f --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.ElapsedTime.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { type t, css, Signal } from './common.ts'; + +export type ElapsedTimeProps = { + player?: t.VideoPlayerSignals; + abs?: t.CssEdgesInput | boolean; + show?: boolean; + style?: t.CssInput; +}; + +type P = ElapsedTimeProps; + +/** + * Component: + */ +export const ElapsedTime: React.FC<P> = (props) => { + const { player, show = true } = props; + const currentTime = player?.props.currentTime.value ?? 0; + + Signal.useRedrawEffect(() => player?.props.currentTime.value); + if (!show) return null; + if (currentTime <= 0) return null; + + /** + * Render: + */ + const styles = { + base: css({ + Absolute: wrangle.abs(props), + userSelect: 'none', + fontSize: 11, + opacity: 0.5, + }), + }; + + return <div className={css(styles.base, props.style).class}>{formatTime(currentTime)}</div>; +}; + +/** + * Helpers + */ +const formatTime = (timeInSeconds: number): string => { + const mins = Math.floor(timeInSeconds / 60); + const secs = Math.floor(timeInSeconds % 60); + const centi = Math.floor((timeInSeconds % 1) * 100); + const fmt = (value: number) => String(value).padStart(2, '0'); + return `${fmt(mins)}:${fmt(secs)}:${fmt(centi)}`; +}; + +/** + * Helpers + */ +const wrangle = { + abs(props: P): t.CssEdgesInput { + const { abs } = props; + if (!abs) return; + if (abs === true) return [5, 6, null, null]; + if (Array.isArray(abs)) return abs; + return; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/-SPEC.Debug.tsx new file mode 100644 index 0000000000..8b3225728b --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/-SPEC.Debug.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Lorem } from '../../-test.ui.ts'; +import { type t, Button, css, Signal } from './common.ts'; + +type P = t.FadeTextProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + text: s<P['text']>('Lorem'), + loremIndex: s(0), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.text.value; + p.loremIndex.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'FadeText'}</div> + </div> + + <Button + block + label={() => `theme: ${p.theme.value ?? '<undefined>'}`} + onClick={() => Signal.cycle<P['theme']>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + + <Button + block + label={() => { + const value = p.text.value; + return `text: ${value ? `"${value}"` : '<undefined>'}`; + }} + onClick={() => { + const index = (p.loremIndex.value += 1); + p.text.value = Lorem.text.split(' ')[index]; + }} + /> + + <Button + block + label={() => `text: (clear)`} + onClick={() => { + p.text.value = undefined; + }} + /> + + <Button + block + label={() => `text: (multi-line)`} + onClick={() => { + p.text.value = 'multi\nline'; + }} + /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/-SPEC.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/-SPEC.tsx new file mode 100644 index 0000000000..83835c0d34 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/-SPEC.tsx @@ -0,0 +1,28 @@ +import { Dev, Spec, Signal, Lorem } from '../../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { FadeText } from './mod.ts'; + +export default Spec.describe('FadeText', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size([390, 200]) + .display('grid') + .render((e) => <FadeText theme={p.theme.value} text={p.text.value} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/common.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/common.ts new file mode 100644 index 0000000000..2bb91a0351 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/common.ts @@ -0,0 +1,13 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { + fontSize: 36, + fontWeight: 'bold', + letterSpacing: '-0.02em', + lineHeight: 0.95, + duration: 700, +} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/mod.ts new file mode 100644 index 0000000000..e1484206a6 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { FadeText } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/t.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/t.ts new file mode 100644 index 0000000000..931e3c0220 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/t.ts @@ -0,0 +1,17 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type FadeTextProps = { + text?: string; + duration?: t.Msecs; + + // Style: + fontSize?: t.CssProps['fontSize']; + fontWeight?: t.CssProps['fontWeight']; + lineHeight?: t.CssProps['lineHeight']; + letterSpacing?: t.CssProps['letterSpacing']; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/ui.Item.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/ui.Item.tsx new file mode 100644 index 0000000000..16406dd6c4 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/ui.Item.tsx @@ -0,0 +1,50 @@ +import React, { useEffect, useState } from 'react'; +import { type t, css, Time, ReactString } from './common.ts'; + +export type FadeItem = { id: number; text: string; fadingOut: boolean }; + +export type ItemProps = { + item: FadeItem; + duration: number; + style?: t.CssInput; +}; + +/** + * Component: + */ +export const Item: React.FC<ItemProps> = (props) => { + const { item, duration } = props; + const [visible, setVisible] = useState<boolean>(false); + + /** + * Effect: fade-out + */ + useEffect(() => { + if (!item.fadingOut) { + const time = Time.until(); + time.delay(0, () => setVisible(true)); + return time.dispose; + } + }, [item.fadingOut]); + + /** + * Effect: When the item is marked as fading out, update the visible flag. + */ + useEffect(() => { + if (item.fadingOut) setVisible(false); + }, [item.fadingOut]); + + /** + * Render: + */ + const styles = { + base: css({ display: 'grid', placeItems: 'center' }), + body: css({ transition: `opacity ${duration}ms`, opacity: visible ? 1 : 0 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.body.class}>{ReactString.break(item.text)}</div> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/ui.tsx new file mode 100644 index 0000000000..04c8145605 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.FadeText/ui.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Color, css, D, Time, type t } from './common.ts'; +import { Item, type FadeItem } from './ui.Item.tsx'; + +export const FadeText: React.FC<t.FadeTextProps> = (props) => { + const { + text = '', + fontSize = D.fontSize, + fontWeight = D.fontWeight, + letterSpacing = D.letterSpacing, + lineHeight = D.lineHeight, + duration = D.duration, + } = props; + + const [items, setItems] = useState<FadeItem[]>([{ id: 0, text, fadingOut: false }]); + const next = useRef(1); + + /** + * Effect: + */ + useEffect(() => { + setItems((prev) => { + if (prev.length && prev[prev.length - 1].text === text) return prev; // ā If the latest item already shows the same text, do nothing. + const updated = prev.map((item) => ({ ...item, fadingOut: true })); // ā Mark all existing items as fading out. + return [...updated, { id: next.current++, text, fadingOut: false }]; // ā Add the new text item with a unique id. + }); + + // Schedule removal of items that have faded out. + const time = Time.until(); + time.delay(duration, () => setItems((prev) => prev.filter((item) => !item.fadingOut))); + return time.dispose; + }, [text, duration]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const color = theme.fg; + const styles = { + base: css({ position: 'relative' }), + item: css({ Absolute: 0, color, fontSize, fontWeight, letterSpacing, lineHeight }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + {items.map((item) => { + return <Item key={item.id} item={item} duration={duration} style={styles.item} />; + })} + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/-SPEC.Debug.tsx new file mode 100644 index 0000000000..3c9d8c3b26 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/-SPEC.Debug.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Images } from '../../ui.Overview/mod.ts'; +import { type t, Button, css, Signal } from './common.ts'; + +type P = t.ImageViewProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + padding: s<P['padding']>(), + src: s<string>(), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.src.value; + p.padding.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'Image'}</div> + </div> + + <Button + block + label={() => `theme: ${p.theme.value ?? '<undefined>'}`} + onClick={() => Signal.cycle<P['theme']>(p.theme, ['Light', 'Dark'])} + /> + + <Button + block + label={() => { + const value = p.src.value; + return `src: ${value ? `...${value.slice(-36)}` : '<undefined>'}`; + }} + onClick={async () => { + const images = await Promise.all(Object.values(Images).map((loader) => loader())); + Signal.cycle<P['src']>(p.src, images); + }} + /> + + <Button + block + label={() => `padding: ${p.padding.value ?? '<undefined>'}`} + onClick={() => { + Signal.cycle<P['padding']>(p.padding, [undefined, 30, '20%', [10, 20, 50, 80]]); + }} + /> + + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/-SPEC.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/-SPEC.tsx new file mode 100644 index 0000000000..cf650198e0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/-SPEC.tsx @@ -0,0 +1,30 @@ +import { Dev, Signal, Spec } from '../../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Image } from './mod.ts'; + +export default Spec.describe('Image', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size('fill', [150, 100]) + .display('grid') + .render((e) => { + return <Image.View theme={p.theme.value} src={p.src.value} padding={p.padding.value} />; + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/common.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/common.ts new file mode 100644 index 0000000000..a45006948c --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/common.ts @@ -0,0 +1,7 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/m.Image.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/m.Image.ts new file mode 100644 index 0000000000..a4b9978011 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/m.Image.ts @@ -0,0 +1,6 @@ +import { type t } from './common.ts'; +import { ImageView as View } from './ui.tsx'; + +export const Image: t.ImageLib = { + View, +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/mod.ts new file mode 100644 index 0000000000..68587a4576 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { Image } from './m.Image.ts'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/t.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/t.ts new file mode 100644 index 0000000000..ec0552c853 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/t.ts @@ -0,0 +1,18 @@ +import type { t } from './common.ts'; + +/** + * Image content render library. + */ +export type ImageLib = { + View: React.FC<t.ImageViewProps>; +}; + +/** + * <Component>: + */ +export type ImageViewProps = { + src?: string | Promise<string>; + padding?: t.CssEdgesInput; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/ui.tsx new file mode 100644 index 0000000000..692afe0e82 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Image/ui.tsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import { type t, Is, Color, css, Icons, Spinners } from './common.ts'; + +type P = t.ImageViewProps; + +export const ImageView: React.FC<P> = (props) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [src, setSrc] = useState<string>(); + + /** + * Handlers: + */ + const handleLoad = () => { + setLoading(false); + }; + + const handleError = () => { + setLoading(false); + setError(true); + }; + + /** + * Effects: + */ + useEffect(() => { + wrangle.src(props.src, setSrc); + }, [props.src]); + + useEffect(() => { + setLoading(true); + setError(false); + }, [src]); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ color: theme.fg, display: 'grid' }), + img: css({ + Absolute: [-99999, null, null, -99999], + opacity: 0, + pointerEvents: 'none', + }), + display: { + base: css({ Absolute: props.padding, display: 'grid' }), + img: css({ + backgroundImage: src ? `url(${src})` : undefined, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + backgroundSize: 'contain', + opacity: loading || error ? 0 : 1, + transition: `opacity 400ms`, + }), + }, + spinner: css({ + Absolute: 0, + display: 'grid', + placeItems: 'center', + pointerEvents: 'none', + }), + error: { + base: css({ + Absolute: 0, + pointerEvents: 'none', + opacity: error ? 1 : 0, + transition: `opacity 400ms`, + display: 'grid', + placeItems: 'center', + }), + body: css({ display: 'grid', placeItems: 'center', rowGap: '12px' }), + }, + }; + + const elImg = ( + <img className={styles.img.class} src={src} onLoad={handleLoad} onError={handleError} /> + ); + + const elDisplayImage = ( + <div className={styles.display.base.class}> + <div className={styles.display.img.class} /> + </div> + ); + + const elSpinner = loading && ( + <div className={styles.spinner.class}> + <Spinners.Bar theme={props.theme} /> + </div> + ); + + const elError = error && ( + <div className={styles.error.base.class}> + <div className={styles.error.body.class}> + <Icons.Error size={38} /> + <div>{'( failed to load image )'}</div> + </div> + </div> + ); + + return ( + <div className={css(styles.base, props.style).class}> + {elImg} + {elDisplayImage} + {elSpinner} + {elError} + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + async src(value: P['src'], setState: (value?: string) => void) { + setState(Is.promise(value) ? await value : value); + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/common.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/common.ts new file mode 100644 index 0000000000..2bb91a0351 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/common.ts @@ -0,0 +1,13 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { + fontSize: 36, + fontWeight: 'bold', + letterSpacing: '-0.02em', + lineHeight: 0.95, + duration: 700, +} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/mod.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/mod.ts new file mode 100644 index 0000000000..cf59c44b62 --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/mod.ts @@ -0,0 +1,2 @@ +export { Pulldown } from './ui.tsx'; +export { usePulldown } from './use.Pulldown.tsx'; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/t.ts b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/t.ts new file mode 100644 index 0000000000..0c2cfcf45f --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/t.ts @@ -0,0 +1,9 @@ +import { type t } from './common.ts'; + +/** + * Hook for managing the pulldown. + */ +export type UsePulldown = (props: t.VideoContentProps, timestamp: t.TimestampsHook) => PulldownHook; +export type PulldownHook = { + readonly is: { readonly showing: boolean }; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/ui.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/ui.tsx new file mode 100644 index 0000000000..00cb3f904b --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/ui.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { ElapsedTime } from '../ui.ElapsedTime.tsx'; +import { useTimestamps } from '../use.Timestamps.ts'; +import { type t, Cropmarks, css, Sheet } from './common.ts'; + +export type PullDownProps = t.VideoContentProps; + +/** + * Component: + */ +export const Pulldown: React.FC<PullDownProps> = (props) => { + const { state, content } = props; + const { showElapsed = true } = content; + + const player = content.video; + const breakpoint = state.breakpoint; + const timestamp = useTimestamps(props, player); + + /** + * Render: + */ + const gutter = breakpoint.name === 'Desktop' ? 40 : 10; + const edgeTrack = `minmax(${gutter}px, 1fr)`; + const centerTrack = `minmax(0px, 960px)`; + const edge: t.SheetMarginInput = [edgeTrack, centerTrack, edgeTrack]; + + const styles = { + base: css({ position: 'relative', marginBottom: 218 }), + body: css({ Absolute: 0, display: 'grid' }), + }; + + const elBody = ( + <div className={styles.body.class}> + <Cropmarks + size={{ mode: 'fill', x: true, y: true, margin: [30, 30, 30, 30] }} + borderOpacity={0.06} + > + {timestamp.pulldown} + </Cropmarks> + </div> + ); + + return ( + <Sheet + {...props} + style={styles.base} + theme={props.theme} + edgeMargin={edge} + orientation={'Top:Down'} + > + {elBody} + <ElapsedTime player={player} abs={[null, 15, 10, null]} show={showElapsed} /> + </Sheet> + ); +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/use.Pulldown.tsx b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/use.Pulldown.tsx new file mode 100644 index 0000000000..6840189d6a --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/ui.Pulldown/use.Pulldown.tsx @@ -0,0 +1,33 @@ +import React, { useCallback, useEffect } from 'react'; +import { type t } from './common.ts'; +import { Pulldown } from './ui.tsx'; + +export const usePulldown: t.UsePulldown = (props, timestamp) => { + const { state, content } = props; + const render = useCallback(() => <Pulldown {...props} />, [props, timestamp]); + + /** + * Effect: show/hide. + */ + useEffect(() => { + const id = `${content.id}:pulldown`; + const exists = state.stack.exists((m) => m.id === id); + + if (exists && !timestamp.pulldown) { + state.stack.pop(); + return; + } + + if (!exists && timestamp.pulldown) { + state.stack.push({ id, render }); + return; + } + }, [timestamp.pulldown, content.id, render]); + + /** + * API + */ + return { + is: { showing: !!timestamp.pulldown }, + }; +}; diff --git a/deploy/@tdb.slc/src/ui.Content/ui/use.Timestamps.ts b/deploy/@tdb.slc/src/ui.Content/ui/use.Timestamps.ts new file mode 100644 index 0000000000..cdcff11bca --- /dev/null +++ b/deploy/@tdb.slc/src/ui.Content/ui/use.Timestamps.ts @@ -0,0 +1,53 @@ +import { useState } from 'react'; +import { type t, Is, Signal, Timestamp } from './common.ts'; + +export const useTimestamps: t.UseTimestamps = (props, player) => { + const { state, content } = props; + + const [column, setColumn] = useState<t.ReactNode>(); + const [pulldown, setPulldown] = useState<t.ReactNode>(); + + Signal.useEffect(() => { + if (!player || !props.content?.timestamps) return; + + const exists = state.stack.exists((e) => e.id === content.id); + if (!exists) { + setColumn(undefined); + setPulldown(undefined); + return; + } + + const timestamps = props.content.timestamps; + const secs = player.props.currentTime.value; + const match = Timestamp.find(timestamps, secs, { unit: 'secs' }); + + const renderer = wrangle.renderer(match?.data); + render(props, setColumn, renderer.column); + render(props, setPulldown, renderer.pulldown); + }); + + return { + column, + pulldown, + }; +}; + +/** + * Helpers + */ +const wrangle = { + renderer(data?: t.ContentTimestamp): t.ContentTimestampProps { + if (!data) return {}; + if (typeof data === 'function') return { column: data }; + return data; + }, +} as const; + +async function render( + props: t.VideoContentProps, + setState: (value: t.ReactNode) => void, + renderer?: t.VideoContentRenderer, +) { + const res = renderer?.(props); + setState(Is.promise(res) ? await res : res); +} diff --git a/deploy/@tdb.slc/src/ui/-test.ui.ts b/deploy/@tdb.slc/src/ui/-test.ui.ts new file mode 100644 index 0000000000..e5bce83774 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/-test.ui.ts @@ -0,0 +1,7 @@ +/** + * @module + * Testing tools running in the browser/ui. + */ +export { expect } from '@sys/std/testing'; +export { Dev, Lorem, Spec } from '@sys/ui-react-devharness'; +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui/App.Layout/common.ts b/deploy/@tdb.slc/src/ui/App.Layout/common.ts new file mode 100644 index 0000000000..579fb2c8be --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Layout/common.ts @@ -0,0 +1,3 @@ +export * from '../common.ts'; + +export const DEFAULTS = {}; diff --git a/deploy/@tdb.slc/src/ui/App.Layout/m.Breakpoint.ts b/deploy/@tdb.slc/src/ui/App.Layout/m.Breakpoint.ts new file mode 100644 index 0000000000..e80662dcd0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Layout/m.Breakpoint.ts @@ -0,0 +1,38 @@ +import { type t } from './common.ts'; + +/** + * Layout breakpoints calculation/helpers. + */ +export const Breakpoint: t.BreakpointLib = { + from(input: t.BreakpointSizeInput): t.Breakpoint { + if (typeof input === 'number') return Breakpoint.fromWidth(input); + if (typeof input === 'string') return Breakpoint.fromName(input); + return input; + }, + + fromWidth(width?: number): t.Breakpoint { + const name = Breakpoint.name(width); + return Breakpoint.fromName(name); + }, + + fromName(name: t.BreakpointName): t.Breakpoint { + const is = Breakpoint.is(name); + return { name, is }; + }, + + name(width?: number): t.BreakpointName { + if (width == null || width < 0) return 'UNKNOWN'; // NB: pre useSizeObserver initial measurement. + if (width <= 430) return 'Mobile'; + if (width <= 767) return 'Intermediate'; + return 'Desktop'; + }, + + is(name: t.BreakpointName): t.Breakpoint['is'] { + return { + ready: name !== 'UNKNOWN', + mobile: name === 'Mobile', + intermediate: name === 'Intermediate', + desktop: name === 'Desktop', + }; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/App.Layout/m.Layout.tsx b/deploy/@tdb.slc/src/ui/App.Layout/m.Layout.tsx new file mode 100644 index 0000000000..bca7ab9b88 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Layout/m.Layout.tsx @@ -0,0 +1,9 @@ +import { type t } from './common.ts'; +import { Breakpoint } from './m.Breakpoint.ts'; + +/** + * Main Layout API (logic). + */ +export const Layout: t.AppLayoutLib = { + Breakpoint, +} as const; diff --git a/deploy/@tdb.slc/src/ui/App.Layout/mod.ts b/deploy/@tdb.slc/src/ui/App.Layout/mod.ts new file mode 100644 index 0000000000..4f8139da39 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Layout/mod.ts @@ -0,0 +1,6 @@ +/** + * @module + * General (non-UI component) layout logic. + */ +export { Breakpoint } from './m.Breakpoint.ts'; +export { Layout } from './m.Layout.tsx'; diff --git a/deploy/@tdb.slc/src/ui/App.Layout/t.Breakpoint.ts b/deploy/@tdb.slc/src/ui/App.Layout/t.Breakpoint.ts new file mode 100644 index 0000000000..c4609fe3a1 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Layout/t.Breakpoint.ts @@ -0,0 +1,41 @@ +import { type t } from './common.ts'; + +export type BreakpointSizeInput = t.NumberWidth | t.BreakpointName | t.Breakpoint; + +/** + * Library for working with breakpoints. + */ +export type BreakpointLib = { + from(input: t.BreakpointSizeInput): t.Breakpoint; + fromWidth(width?: number): t.Breakpoint; + fromName(name: t.BreakpointName): t.Breakpoint; + name(width?: number): t.BreakpointName; + is(name: t.BreakpointName): t.Breakpoint['is']; +}; + +/** + * Layout breakpoints + */ +export type BreakpointName = 'Mobile' | 'Intermediate' | 'Desktop' | 'UNKNOWN'; +export type Breakpoint = { + name: BreakpointName; + is: { + /** Width is -1 prior to the ResizeObserver completing it's first measure pass. */ + ready: boolean; + /** + * Mobile breakpoint. + * True when the viewport width is 430px or less, typically for smartphones. + */ + mobile: boolean; + /** + * Intermediate breakpoint. + * True when the viewport width is between 431px and 767px, ideal for phablets or small tablets. + */ + intermediate: boolean; + /** + * Desktop breakpoint. + * True when the viewport width is 768px or greater, typically for desktops and larger tablets. + */ + desktop: boolean; + }; +}; diff --git a/deploy/@tdb.slc/src/ui/App.Layout/t.ts b/deploy/@tdb.slc/src/ui/App.Layout/t.ts new file mode 100644 index 0000000000..cf598c3269 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Layout/t.ts @@ -0,0 +1,10 @@ +import { type t } from './common.ts'; +export type * from './t.Breakpoint.ts'; + +/** + * Logical helpers for working with layouts. + */ +export type AppLayoutLib = { + /** The screen-size breakpoint library. */ + readonly Breakpoint: t.BreakpointLib; +}; diff --git a/deploy/@tdb.slc/src/ui/App.Render/-.test.ts b/deploy/@tdb.slc/src/ui/App.Render/-.test.ts new file mode 100644 index 0000000000..33a11f90ea --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/-.test.ts @@ -0,0 +1,4 @@ +import { type t, describe, expect, it } from '../../-test.ts'; +import { AppSignals } from '../App.Signals/mod.ts'; + +describe('AppRender', () => {}); diff --git a/deploy/@tdb.slc/src/ui/App.Render/common.ts b/deploy/@tdb.slc/src/ui/App.Render/common.ts new file mode 100644 index 0000000000..8007245bca --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/common.ts @@ -0,0 +1,16 @@ +import { type t } from './common.ts'; +export * from '../common.ts'; + +export { AppSignals } from '../App.Signals/mod.ts'; +export { Breakpoint } from '../App.Layout/mod.ts'; + +/** + * Constants + */ +export const DEFAULTS = { + get theme() { + const base: t.CommonTheme = 'Dark'; + const sheet: t.CommonTheme = 'Light'; + return { base, sheet }; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/App.Render/m.Render.preload.tsx b/deploy/@tdb.slc/src/ui/App.Render/m.Render.preload.tsx new file mode 100644 index 0000000000..2644f8f5d2 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/m.Render.preload.tsx @@ -0,0 +1,25 @@ +import { type t, Preload } from './common.ts'; +import { render } from './m.Render.stack.tsx'; + +/** + * Ensure the specified ESM content modules have been dyanamically imported. + */ +export const preload: t.AppRenderLib['preload'] = async <T extends string>( + state: t.AppSignals, + factory: (flag: T) => Promise<t.Content | undefined>, + ...content: T[] +) => { + if (typeof document === 'undefined') return; + + /** + * Dynamic import of ESM: + */ + const loading = content.map((flag) => factory(flag)); + const imports = (await Promise.all(loading)) as t.Content[]; + const elements = imports.map((content, index) => render({ index, content, state })); + + /** + * Render Portal: + */ + Preload.render(elements, 5_000); +}; diff --git a/deploy/@tdb.slc/src/ui/App.Render/m.Render.stack.tsx b/deploy/@tdb.slc/src/ui/App.Render/m.Render.stack.tsx new file mode 100644 index 0000000000..3b0768ba36 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/m.Render.stack.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { type t, AnimatePresence, css, DEFAULTS } from './common.ts'; + +/** + * Renders the body of the matching timestamp. + */ +export function stack(state: t.AppSignals | undefined): t.ReactNode { + if (!state) return []; + const stack = state.stack.items ?? []; + const nodes = stack.map((content, index) => render({ index, state, content })); + return <AnimatePresence>{nodes}</AnimatePresence>; +} + +/** + * Render a single level in the stack. + */ +export function render(args: { index: number; state: t.AppSignals; content: t.Content }) { + const { state, content, index } = args; + const theme = wrangle.theme(content, state); + const is = wrangle.is(state, index); + const el = content.render?.({ index, content, state, is, theme }); + + const style = css({ + Absolute: 0, + pointerEvents: 'none', + zIndex: 0, // NB: establish a new stacking context (prevents content jumping above higher stack levels). + display: 'grid', + }); + + return ( + <div key={`${content.id}.${index}`} className={style.class}> + {el} + </div> + ); +} + +/** + * Helpers + */ +const wrangle = { + theme(content: t.Content, state: t.AppSignals): t.CommonTheme { + // NB: hard-coded from default. + // Possibly expand this later to store theme state on the App signals. + return DEFAULTS.theme.base; + }, + + is(state: t.AppSignals, index: number): t.ContentFlags { + const top = index === state.stack.length - 1; + const bottom = index === 0; + return { top, bottom }; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/App.Render/m.Render.ts b/deploy/@tdb.slc/src/ui/App.Render/m.Render.ts new file mode 100644 index 0000000000..7a90ac21ba --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/m.Render.ts @@ -0,0 +1,11 @@ +import { type t } from './common.ts'; +import { stack } from './m.Render.stack.tsx'; +import { preload } from './m.Render.preload.tsx'; + +/** + * Render functions for display content. + */ +export const AppRender: t.AppRenderLib = { + stack, + preload, +} as const; diff --git a/deploy/@tdb.slc/src/ui/App.Render/mod.ts b/deploy/@tdb.slc/src/ui/App.Render/mod.ts new file mode 100644 index 0000000000..9af91c36d9 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + */ +export { DEFAULTS } from './common.ts'; +export { AppRender } from './m.Render.ts'; diff --git a/deploy/@tdb.slc/src/ui/App.Render/t.ts b/deploy/@tdb.slc/src/ui/App.Render/t.ts new file mode 100644 index 0000000000..514b7105cc --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Render/t.ts @@ -0,0 +1,17 @@ +import type { t } from './common.ts'; + +/** + * Tools for rendering the application structure. + */ +export type AppRenderLib = { + stack: (state: t.AppSignals | undefined) => t.ReactNode; + + /** + * Ensure the specified ESM content modules have been dyanamically imported. + */ + preload<T extends string>( + state: t.AppSignals, + factory: (flag: T) => Promise<t.Content | undefined>, + ...content: T[] + ): Promise<void>; +}; diff --git a/deploy/@tdb.slc/src/ui/App.Signals.Controller/-.test.ts b/deploy/@tdb.slc/src/ui/App.Signals.Controller/-.test.ts new file mode 100644 index 0000000000..f9ea3adcee --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals.Controller/-.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it, rx } from '../../-test.ts'; +import { AppSignals } from '../App.Signals/mod.ts'; + +describe('AppSignals.Controllers', () => { + describe('start', () => { + it('should add all children', () => { + const state = AppSignals.create(); + + const root = AppSignals.Controllers.start(state); + expect(root.disposed).to.eql(false); + expect(root.kind).to.eql('Controller:App'); + + expect(root.children.map((e) => e.kind)).to.eql(['Controller:App:Background']); + root.children.forEach((child) => expect(child.disposed).to.eql(false)); + + const listening = state.props.controllers.listening.value; + expect(listening.includes('Controller:App')).to.eql(true); + expect(listening.includes('Controller:App:Background')).to.eql(true); + + root.dispose(); + expect(root.disposed).to.eql(true); + root.children.forEach((child) => expect(child.disposed).to.eql(true)); + }); + }); + + describe('background', () => { + it('adjusts backbround blur when stack changes', () => { + const life = rx.disposable(); + const state = AppSignals.create(); + const p = state.props; + expect(p.background.video.blur.value).to.eql(0); + + const ctrl = AppSignals.Controllers.background(state, life); + expect(ctrl.disposed).to.eql(false); + expect(ctrl.kind).to.eql('Controller:App:Background'); + expect(ctrl.children).to.eql([]); + + const listening = state.props.controllers.listening.value; + expect(listening.includes('Controller:App:Background')).to.eql(true); + + // Add to the stack. + state.stack.push({ id: 'base' }); + expect(p.background.video.blur.value).to.eql(0); // NB: no change. + + state.stack.push({ id: 'one' }); + expect(p.background.video.blur.value).to.eql(20); // NB: no change. + + state.stack.clear(); + expect(p.background.video.blur.value).to.eql(0); + + life.dispose(); + expect(ctrl.disposed).to.eql(true); + + state.stack.push({ id: 'one' }, { id: 'two' }); + expect(p.background.video.blur.value).to.eql(0); // NB: no change - disposed. + }); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/App.Signals.Controller/common.ts b/deploy/@tdb.slc/src/ui/App.Signals.Controller/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals.Controller/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui/App.Signals.Controller/m.Controllers.background.ts b/deploy/@tdb.slc/src/ui/App.Signals.Controller/m.Controllers.background.ts new file mode 100644 index 0000000000..12c798dedd --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals.Controller/m.Controllers.background.ts @@ -0,0 +1,25 @@ +import { type t, Signal, rx } from './common.ts'; + +export const background: t.AppControllersLib['background'] = (state, until$) => { + const kind: t.AppControllerKind = 'Controller:App:Background'; + const listeners = Signal.listeners(until$); + const controllers = state.props.controllers; + controllers.listening.value = [...controllers.listening.value, kind]; + + /** + * Blur background when higher layers are visible. + */ + listeners.effect(() => { + const totalLayers = state.stack.length; + const blur = state.props.background.video.blur; + blur.value = totalLayers > 1 ? 20 : 0; + }); + + /** + * API + */ + return rx.toLifecycle<t.AppController>(listeners, { + kind, + children: [], + }); +}; diff --git a/deploy/@tdb.slc/src/ui/App.Signals.Controller/m.Controllers.ts b/deploy/@tdb.slc/src/ui/App.Signals.Controller/m.Controllers.ts new file mode 100644 index 0000000000..707e2a3bcf --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals.Controller/m.Controllers.ts @@ -0,0 +1,27 @@ +import { type t, Signal, rx } from './common.ts'; +import { background } from './m.Controllers.background.ts'; + +export const Controllers: t.AppControllersLib = { + background, + + start(state, until$) { + const kind: t.AppControllerKind = 'Controller:App'; + const children = new Set<t.AppController>(); + const listeners = Signal.listeners(until$); + const controllers = state.props.controllers; + controllers.listening.value = [...controllers.listening.value, kind]; + + // Initialize child controllers. + children.add(background(state, listeners.dispose$)); + + /** + * API: + */ + return rx.toLifecycle<t.AppController>(listeners, { + kind, + get children() { + return [...children]; + }, + }); + }, +}; diff --git a/deploy/@tdb.slc/src/ui/App.Signals.Controller/mod.ts b/deploy/@tdb.slc/src/ui/App.Signals.Controller/mod.ts new file mode 100644 index 0000000000..8ef8f105e7 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals.Controller/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Behavior controllers that manipulate a signals API. + */ +export { Controllers } from './m.Controllers.ts'; diff --git a/deploy/@tdb.slc/src/ui/App.Signals.Controller/t.ts b/deploy/@tdb.slc/src/ui/App.Signals.Controller/t.ts new file mode 100644 index 0000000000..9255532a82 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals.Controller/t.ts @@ -0,0 +1,26 @@ +import type { t } from './common.ts'; + +/** + * Signal controllers for the application state. + */ +export type AppControllersLib = { + /** Hooks up all baseline controllers */ + start(state: t.AppSignals, until$?: t.UntilInput): AppController; + + /** + * Hook into the given state and apply controller logic + * based on live changes to the application. + */ + background(state: t.AppSignals, until$?: t.UntilInput): AppController; +}; + +/** + * Common interface for application controllers. + */ +export type AppController = t.Lifecycle & { + readonly kind: AppControllerKind; + readonly children: AppController[]; +}; + +/** Controller identification codes. */ +export type AppControllerKind = 'Controller:App' | 'Controller:App:Background'; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/-.test.ts b/deploy/@tdb.slc/src/ui/App.Signals/-.test.ts new file mode 100644 index 0000000000..b2eb478d5b --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/-.test.ts @@ -0,0 +1,55 @@ +import { c, describe, expect, it, Signal } from '../../-test.ts'; +import { VIDEO } from '../../ui.Content/VIDEO.ts'; +import { AppSignals } from './mod.ts'; + +describe('AppSignals', () => { + describe('lifecycle', () => { + it('create', () => { + const app = AppSignals.create(); + const p = app.props; + + expect(p.dist.value).to.eql(undefined); + expect(p.screen.breakpoint.value).to.eql('UNKNOWN'); + expect(p.stack.value).to.eql([]); + expect(p.screen.breakpoint.value).to.eql(app.breakpoint.name); + expect(p.controllers.listening.value).to.eql([]); + + expect(p.background.video.src.value).to.eql(VIDEO.Tubes.src); + expect(p.background.video.playing.value).to.eql(true); + expect(p.background.video.opacity.value).to.eql(0.2); + expect(p.background.video.blur.value).to.eql(0); + + console.info(); + console.info(c.brightGreen('SLC:App.Signals:')); + console.info(app); + console.info(); + }); + + it('instance id', () => { + const a = AppSignals.create(); + const b = AppSignals.create(); + + expect(a.instance.startsWith('app-')).to.eql(true); + expect(a.instance.length).to.be.greaterThan('app-1ss9'.length); + expect(a.instance).to.not.eql(b.instance); + }); + }); + + describe('listen', () => { + it('listens to changes', () => { + const app = AppSignals.create(); + + let count = 0; + const dispose = Signal.effect(() => { + app.listen(); + count++; + }); + + expect(count).to.eql(1); + app.stack.push({ id: 'foo' }); + expect(count).to.eql(2); + + dispose(); + }); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/App.Signals/-Controllers.test.ts b/deploy/@tdb.slc/src/ui/App.Signals/-Controllers.test.ts new file mode 100644 index 0000000000..4acf98d1c0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/-Controllers.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, rx } from '../../-test.ts'; +import { AppSignals } from './mod.ts'; + +describe('AppSignals.Controllers', () => { + describe('start', () => { + it('should add all children', () => { + const state = AppSignals.create(); + + const root = AppSignals.Controllers.start(state); + expect(root.disposed).to.eql(false); + expect(root.kind).to.eql('Controller:App'); + + expect(root.children.map(({ kind: id }) => id)).to.eql(['Controller:App:Background']); + root.children.forEach((child) => expect(child.disposed).to.eql(false)); + + root.dispose(); + expect(root.disposed).to.eql(true); + root.children.forEach((child) => expect(child.disposed).to.eql(true)); + }); + }); + + describe('background', () => { + it('adjusts backbround blur when stack changes', () => { + const life = rx.disposable(); + const state = AppSignals.create(); + const p = state.props; + expect(p.background.video.blur.value).to.eql(0); + + const ctrl = AppSignals.Controllers.background(state, life); + expect(ctrl.disposed).to.eql(false); + expect(ctrl.kind).to.eql('Controller:App:Background'); + expect(ctrl.children).to.eql([]); + + // Add to the stack. + state.stack.push({ id: 'base' }); + expect(p.background.video.blur.value).to.eql(0); // NB: no change. + + state.stack.push({ id: 'one' }); + expect(p.background.video.blur.value).to.eql(20); // NB: no change. + + state.stack.clear(); + expect(p.background.video.blur.value).to.eql(0); + + life.dispose(); + expect(ctrl.disposed).to.eql(true); + + state.stack.push({ id: 'one' }, { id: 'two' }); + expect(p.background.video.blur.value).to.eql(0); // NB: no change - disposed. + }); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/App.Signals/-Stack.test.ts b/deploy/@tdb.slc/src/ui/App.Signals/-Stack.test.ts new file mode 100644 index 0000000000..01f69781ce --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/-Stack.test.ts @@ -0,0 +1,46 @@ +import { type t, describe, expect, it, Signal } from '../../-test.ts'; +import { AppSignals } from './mod.ts'; + +describe('AppSignals.stack', () => { + const a: t.Content = { id: 'a' }; + const b: t.Content = { id: 'b' }; + const c: t.Content = { id: 'b' }; + + /** + * NOTE: stack more fully tested in ui/components:Sheet + */ + it('push ā pop ā clear', () => { + const app = AppSignals.create(); + const fired: number[] = []; + const dispose = Signal.effect(() => { + fired.push(app.props.stack.value.length); + }); + + expect(app.stack.length).to.eql(0); + + // Push single. + app.stack.push(a); + expect(app.stack.length).to.eql(1); + expect(app.props.stack.value).to.eql([a]); + expect(fired).to.eql([0, 1]); + + // Push many. + app.stack.push(b, c); + expect(app.stack.length).to.eql(3); + expect(app.props.stack.value).to.eql([a, b, c]); + expect(fired).to.eql([0, 1, 3]); + + // Push <undefined>. + app.stack.push(); + app.stack.push(undefined, a, undefined, b); + expect(app.props.stack.value).to.eql([a, b, c, a, b]); + + app.stack.clear(1); + expect(app.props.stack.value).to.eql([a]); + + app.stack.clear(); + expect(app.props.stack.value).to.eql([]); + + dispose(); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/App.Signals/common.ts b/deploy/@tdb.slc/src/ui/App.Signals/common.ts new file mode 100644 index 0000000000..259b40d01f --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/common.ts @@ -0,0 +1,2 @@ +export { Breakpoint } from '../App.Layout/mod.ts'; +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/m.Signals.create.ts b/deploy/@tdb.slc/src/ui/App.Signals/m.Signals.create.ts new file mode 100644 index 0000000000..dd3090df1d --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/m.Signals.create.ts @@ -0,0 +1,55 @@ +import { type t, Breakpoint, SheetBase, Signal, slug, TUBES } from './common.ts'; + +/** + * Create a new instance of the application-state signals API. + */ +export const create: t.AppSignalsLib['create'] = (until$) => { + const s = Signal.create; + + /** + * API: + */ + type T = t.AppSignals; + type P = T['props']; + const props: P = { + dist: s<t.DistPkg>(), + stack: s<t.Content[]>([]), + screen: { breakpoint: s<t.BreakpointName>('UNKNOWN') }, + background: { + video: { + src: s<string>(TUBES.src), + playing: s<boolean>(true), + opacity: s<t.Percent | undefined>(0.2), + blur: s<t.Percent | undefined>(0), + }, + }, + controllers: { listening: s<t.AppControllerKind[]>([]) }, + }; + + const stack = SheetBase.Signals.stack(props.stack); + const api: T = { + instance: `app-${slug()}`, + get props() { + return props; + }, + get stack() { + return stack; + }, + get breakpoint() { + return Breakpoint.from(props.screen.breakpoint.value); + }, + listen() { + const p = props; + p.stack.value; + p.dist.value; + p.screen.breakpoint.value; + p.background.video.src.value; + p.background.video.playing.value; + p.background.video.opacity.value; + p.background.video.blur.value; + }, + }; + + // Finish up. + return api; +}; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/m.Signals.ts b/deploy/@tdb.slc/src/ui/App.Signals/m.Signals.ts new file mode 100644 index 0000000000..4795031007 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/m.Signals.ts @@ -0,0 +1,8 @@ +import type { t } from './common.ts'; +import { Controllers } from '../App.Signals.Controller/mod.ts'; +import { create } from './m.Signals.create.ts'; + +export const AppSignals: t.AppSignalsLib = { + Controllers, + create, +}; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/mod.ts b/deploy/@tdb.slc/src/ui/App.Signals/mod.ts new file mode 100644 index 0000000000..90f4eb9f77 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + * Application state (via Signals). + */ +export { AppSignals } from './m.Signals.ts'; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/t.Content.ts b/deploy/@tdb.slc/src/ui/App.Signals/t.Content.ts new file mode 100644 index 0000000000..43336418fb --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/t.Content.ts @@ -0,0 +1,46 @@ +import type { t } from './common.ts'; + +/** + * Definition of content structure. + */ +export type Content<P = {}> = P & { + /** Content identifier. */ + id: t.StringId; + + /** + * Render the base content. + * Additional items (such as the current timestamp) are + * rendered into the {children} property. + */ + render?(props: ContentProps<P>): t.ReactNode; +}; + +/** + * Component Props: + */ +export type ContentProps<P = {}> = { + /** The index within the content-stack. */ + index: t.Index; + is: t.ContentFlags; + content: t.Content<P>; + state: t.AppSignals; + theme: t.CommonTheme; + style?: t.CssInput; +}; + +/** + * Flags pertaining to content. + */ +export type ContentFlags = { + /** Flag indicating if this is the current top-level view in the stack. */ + top: boolean; + /** Flag indicating if this is the bottom most view in the stack. */ + bottom: boolean; +}; + +/** + * Syncronous of asynchronous content renderer. + */ +export type ContentRenderer<P extends t.ContentProps = t.ContentProps> = ( + props: P, +) => t.ReactNode | Promise<t.ReactNode>; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/t.Signals.ts b/deploy/@tdb.slc/src/ui/App.Signals/t.Signals.ts new file mode 100644 index 0000000000..50f4b4f237 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/t.Signals.ts @@ -0,0 +1,39 @@ +import type { t } from './common.ts'; + +/** + * Global application-state signals API. + */ +export type AppSignals = { + /** Unique instance of the signals. */ + readonly instance: t.StringId; + + /** Signal properties: */ + readonly props: { + readonly dist: t.Signal<t.DistPkg | undefined>; + readonly stack: t.Signal<t.Content[]>; + readonly screen: { readonly breakpoint: t.Signal<t.BreakpointName> }; + readonly background: { + readonly video: { + readonly src: t.Signal<string>; + readonly playing: t.Signal<boolean>; + readonly opacity: t.Signal<t.Percent | undefined>; + readonly blur: t.Signal<t.Pixels | undefined>; + }; + }; + readonly controllers: { + listening: t.Signal<t.AppControllerKind[]>; + }; + }; + + /** API for interacting with the stack. */ + readonly stack: t.AppSignalsStack; + readonly breakpoint: t.Breakpoint; + + /** Hook into all relevant value listeners. */ + listen(): void; +}; + +/** + * API for managing the screen stack. + */ +export type AppSignalsStack = t.SheetSignalStack<t.Content>; diff --git a/deploy/@tdb.slc/src/ui/App.Signals/t.ts b/deploy/@tdb.slc/src/ui/App.Signals/t.ts new file mode 100644 index 0000000000..d93bdc2a15 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App.Signals/t.ts @@ -0,0 +1,14 @@ +import type { t } from './common.ts'; + +export type * from './t.Content.ts'; +export type * from './t.Signals.ts'; + +/** + * Library for working with the [AppSignals] model. + */ +export type AppSignalsLib = { + readonly Controllers: t.AppControllersLib; + + /** Create a new instance of the application-state signals API. */ + create(dispose$?: t.UntilObservable): t.AppSignals; +}; diff --git a/deploy/@tdb.slc/src/ui/App/-.test.ts b/deploy/@tdb.slc/src/ui/App/-.test.ts new file mode 100644 index 0000000000..7e74c930b7 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App/-.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from '../../-test.ts'; +import { AppSignals, Layout } from './common.ts'; +import { App } from './mod.ts'; + +describe('App', () => { + it('API', () => { + expect(App.Signals).to.equal(AppSignals); + expect(App.signals).to.equal(AppSignals.create); + expect(App.Layout).to.equal(Layout); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/App/common.ts b/deploy/@tdb.slc/src/ui/App/common.ts new file mode 100644 index 0000000000..be106c23ec --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App/common.ts @@ -0,0 +1,5 @@ +export * from '../common.ts'; + +export { AppSignals } from '../App.Signals/mod.ts'; +export { Layout } from '../App.Layout/mod.ts'; +export { AppRender } from '../App.Render/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui/App/m.App.ts b/deploy/@tdb.slc/src/ui/App/m.App.ts new file mode 100644 index 0000000000..054c6d032a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App/m.App.ts @@ -0,0 +1,11 @@ +/** + * @module + */ +import { type t, Layout, AppRender as Render, AppSignals as Signals } from './common.ts'; + +export const App: t.AppLib = { + Layout, + Render, + Signals, + signals: Signals.create, +}; diff --git a/deploy/@tdb.slc/src/ui/App/mod.ts b/deploy/@tdb.slc/src/ui/App/mod.ts new file mode 100644 index 0000000000..292d718a02 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export * from './m.App.ts'; diff --git a/deploy/@tdb.slc/src/ui/App/t.ts b/deploy/@tdb.slc/src/ui/App/t.ts new file mode 100644 index 0000000000..d3ba2de088 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/App/t.ts @@ -0,0 +1,12 @@ +import type { t } from './common.ts'; + +/** + * Root Application (logic) API. + */ +export type AppLib = { + readonly Signals: t.AppSignalsLib; + readonly signals: t.AppSignalsLib['create']; + + readonly Layout: t.AppLayoutLib; + readonly Render: t.AppRenderLib; +}; diff --git a/deploy/@tdb.slc/src/ui/common.ts b/deploy/@tdb.slc/src/ui/common.ts new file mode 100644 index 0000000000..45b6d3a168 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/common.ts @@ -0,0 +1 @@ +export * from './common/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui/common/libs.ts b/deploy/@tdb.slc/src/ui/common/libs.ts new file mode 100644 index 0000000000..511f00e514 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/common/libs.ts @@ -0,0 +1,27 @@ +import { motion as Motion } from 'motion/react'; +export { AnimatePresence } from 'motion/react'; +export { Motion as M, Motion }; + +/** + * System + */ +export { Color, css, Style } from '@sys/ui-css'; +export { Keyboard } from '@sys/ui-dom'; +export { + ReactString, + useClickOutside, + useDist, + useIsTouchSupported, + useSizeObserver, +} from '@sys/ui-react'; +export { + Button, + Cropmarks, + ObjectView, + Player, + Preload, + Sheet as SheetBase, + Spinners, + Svg, + VimeoBackground, +} from '@sys/ui-react-components'; diff --git a/deploy/@tdb.slc/src/ui/common/mod.ts b/deploy/@tdb.slc/src/ui/common/mod.ts new file mode 100644 index 0000000000..cd48a34925 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/common/mod.ts @@ -0,0 +1,15 @@ +export * from '../../common.ts'; +export * from './libs.ts'; + +export { Signal } from '@sys/ui-react'; + +/** + * Local + */ +export * from '../ui.Icons.ts'; + +/** + * Common Video Refs + */ +export const vimeo = (id: number) => ({ id, src: `vimeo/${id}` } as const); +export const TUBES = vimeo(499921561); diff --git a/deploy/@tdb.slc/src/ui/mod.ts b/deploy/@tdb.slc/src/ui/mod.ts new file mode 100644 index 0000000000..f7d4ac5d16 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/mod.ts @@ -0,0 +1,13 @@ +/** + * @module + * UI Components. + */ +export { LogoCanvas } from './ui.Logo.Canvas/mod.ts'; +export { LogoWordmark } from './ui.Logo.Wordmark/mod.ts'; +export { VideoBackground } from './ui.Video.Background/mod.ts'; + +export { Landing as Landing1 } from './ui.Landing-1/mod.ts'; +export { Landing as Landing2 } from './ui.Landing-2/mod.ts'; +export { Sheet } from './ui.Sheet/mod.ts'; + +export { App } from './App/mod.ts'; diff --git a/deploy/@tdb.slc/src/ui/ui.Icons.ts b/deploy/@tdb.slc/src/ui/ui.Icons.ts new file mode 100644 index 0000000000..691ac9dbea --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Icons.ts @@ -0,0 +1,16 @@ +import { Icon } from '@sys/ui-react-components'; +import { MdAdd, MdArrowDownward, MdErrorOutline, MdOutlineAddBox } from 'react-icons/md'; +import { PiProjectorScreenLight } from 'react-icons/pi'; + +const icon = Icon.renderer; +export { icon }; + +/** + * Icon collection: + */ +export const Icons = { + Error: icon(MdErrorOutline), + Add: { Plus: icon(MdAdd), Square: icon(MdOutlineAddBox) }, + Arrow: { Down: icon(MdArrowDownward) }, + ProjectorScreen: icon(PiProjectorScreenLight), +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-1/-SPEC.Debug.tsx new file mode 100644 index 0000000000..5051f18397 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/-SPEC.Debug.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { type t, Button, Color, css, Signal } from './common.ts'; + +/** + * Types + */ +export type DebugProps = { + ctx: { debug: DebugSignals; landing: t.LandingSignals }; + style?: t.CssValue; +}; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals + */ +export function createDebugSignals() { + const s = Signal.create; + const props = { theme: s<t.CommonTheme>('Dark') }; + const api = { props }; + return api; +} + +/** + * Component + */ +export const Debug: React.FC<P> = (props) => { + const { ctx } = props; + const d = ctx.debug.props; + const p = ctx.landing.props; + + Signal.useRedrawEffect(() => { + d.theme.value; + p.canvasPosition.value; + p.sidebarVisible.value; + }); + + /** + * Render: + */ + const theme = Color.theme(d.theme.value); + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.title.class}>{'Landing-1'}</div> + <hr /> + + <Button + block + label={`theme: ${d.theme}`} + onClick={() => Signal.cycle(d.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`sidebarVisible: ${p.sidebarVisible}`} + onClick={() => Signal.toggle(p.sidebarVisible)} + /> + + <hr /> + + <Button + block + label={`canvasPosition: "${p.canvasPosition}"`} + onClick={() => { + Signal.cycle<t.LandingCanvasPosition>(p.canvasPosition, ['Center', 'Center:Bottom']); + }} + /> + + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-1/-SPEC.tsx new file mode 100644 index 0000000000..27cf18f6da --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/-SPEC.tsx @@ -0,0 +1,32 @@ +import { Dev, Spec, Signal } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { Landing, signalsFactory } from './mod.ts'; + +export default Spec.describe('Landing', (e) => { + const debug = createDebugSignals(); + const landing = signalsFactory(); + const d = debug.props; + const p = landing.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + Dev.Theme.signalEffect(ctx, d.theme, 1); + Signal.effect(() => { + p.ready.value; + p.sidebarVisible.value; + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => { + return <Landing theme={d.theme.value} signals={landing} />; + }); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug ctx={{ debug, landing }} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/-Signals.test.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/-Signals.test.ts new file mode 100644 index 0000000000..0b22ff2d14 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/-Signals.test.ts @@ -0,0 +1,27 @@ +import { Time, describe, expect, it, Signal } from '../../-test.ts'; +import { DEFAULTS } from './common.ts'; +import { signalsFactory } from './mod.ts'; + +const D = DEFAULTS; + +describe('Landing (Screen): Signals API', () => { + describe('props', () => { + it('initial values (defaults)', () => { + const signals = signalsFactory(); + const p = signals.props; + expect(p.ready.value).to.eql(false); + expect(p.canvasPosition.value === 'Center').to.be.true; + expect(p.sidebarVisible.value === false).to.be.true; + }); + + it('param: custom { defaults }', () => { + const s = signalsFactory({ + canvasPosition: 'Center:Bottom', + sidebarVisible: true, + }); + const p = s.props; + expect(p.canvasPosition.value === 'Center:Bottom').to.be.true; + expect(p.sidebarVisible.value).to.eql(true); + }); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/common.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/common.ts new file mode 100644 index 0000000000..d7460c06d1 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/common.ts @@ -0,0 +1,18 @@ +import { type t } from '../common.ts'; + +export * from '../common.ts'; +export { LogoWordmark } from '../ui.Logo.Wordmark/mod.ts'; + +/** + * Constants + */ +export const DEFAULTS = { + sidebarVisible: false, + get tubes() { + const id = 499921561; + return { id, src: `vimeo/${id}` }; + }, + get canvasPosition(): t.LandingCanvasPosition { + return 'Center'; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/m.Signals.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/m.Signals.ts new file mode 100644 index 0000000000..eae29f0728 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/m.Signals.ts @@ -0,0 +1,18 @@ +import { type t, DEFAULTS, Signal } from './common.ts'; + +type T = t.LandingSignals; +const D = DEFAULTS; + +/** + * Factory: create a new instance of signals + */ +export const signalsFactory: t.LandingSignalsFactory = (defaults = {}) => { + const s = Signal.create; + const props: T['props'] = { + ready: s<boolean>(false), + sidebarVisible: s<boolean>(defaults.sidebarVisible ?? D.sidebarVisible), + canvasPosition: s<t.LandingCanvasPosition>(defaults.canvasPosition ?? D.canvasPosition), + }; + const api: T = { props }; + return api; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/mod.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/mod.ts new file mode 100644 index 0000000000..7a5c555eef --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/mod.ts @@ -0,0 +1,2 @@ +export { Landing } from './ui.tsx'; +export { signalsFactory } from './m.Signals.ts'; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/t.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/t.ts new file mode 100644 index 0000000000..f4d744e6a8 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/t.ts @@ -0,0 +1,33 @@ +import type { t } from './common.ts'; + +export type LandingCanvasPosition = 'Center' | 'Center:Bottom'; + +/** + * Signals: + */ +export type LandingSignalsFactory = (defaults?: LandingSignalsFactoryDefaults) => t.LandingSignals; +/** Defaults passed to the signals API factory. */ +export type LandingSignalsFactoryDefaults = { + canvasPosition?: t.LandingCanvasPosition; + sidebarVisible?: boolean; +}; + +/** + * Signals API for dynamic control of the <VideoPlayer>. + */ +export type LandingSignals = { + props: { + ready: t.Signal<boolean>; + canvasPosition: t.Signal<t.LandingCanvasPosition>; + sidebarVisible: t.Signal<boolean>; + }; +}; + +/** + * <Component>: + */ +export type Landing1Props = { + theme?: t.CommonTheme; + style?: t.CssInput; + signals?: t.LandingSignals; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.Layout.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.Layout.tsx new file mode 100644 index 0000000000..a779947df6 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.Layout.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { type t, Color, css, DEFAULTS, Motion } from './common.ts'; + +export type LayoutProps = { + canvas: { element?: JSX.Element; position?: t.LandingCanvasPosition }; + video: { element?: JSX.Element }; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = LayoutProps; + +/** + * Component: + */ +export const Layout: React.FC<P> = (props) => { + const { canvas, video } = props; + const position = canvas.position ?? DEFAULTS.canvasPosition; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ position: 'relative', color: theme.fg }), + canvas: css({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%) scale(1)', + }), + + video: css({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '85%', + aspectRatio: '16/9', + backgroundColor: 'white', + borderRadius: 15, + overflow: 'hidden', + display: position === 'Center:Bottom' ? 'grid' : 'none', + }), + }; + + /** + * Animation state: + */ + const canvasState = + position === 'Center:Bottom' + ? { top: 'calc(100% - 10px)', transform: 'translate(-50%, -100%) scale(0.65)' } + : { top: '50%', transform: 'translate(-50%, -50%) scale(1.1)' }; + + const backgroundAnimate = + position === 'Center:Bottom' + ? { opacity: 1, top: '50%', transform: 'translate(-50%, calc(-50% - 80px))' } + : { opacity: 0, top: '50%', transform: 'translate(-50%, calc(-50% - 80px))' }; + + /** + * Elements: + */ + const elVideo = ( + <Motion.div + className={styles.video.class} + initial={false} // Prevent initial animation on mount. + animate={backgroundAnimate} + transition={{ type: 'spring', stiffness: 200, damping: 15, mass: 0.8 }} + > + {video.element} + </Motion.div> + ); + + const elCanvas = canvas.element && ( + <Motion.div + className={styles.canvas.class} + initial={false} + animate={canvasState} + transition={{ type: 'spring', stiffness: 200, damping: 10, mass: 0.5 }} + > + {canvas.element} + </Motion.div> + ); + + return ( + <div className={css(styles.base, props.style).class}> + {elVideo} + {elCanvas} + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.Sidebar.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.Sidebar.tsx new file mode 100644 index 0000000000..5fbdf7a243 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.Sidebar.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { type t, Color, css } from './common.ts'; + +export type SidebarProps = { + width?: t.Pixels; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = SidebarProps; + +/** + * Component: + */ +export const Sidebar: React.FC<P> = (props) => { + const { width } = props; + const bgBlur = 20; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + backgroundColor: Color.alpha(theme.fg, 0.06), + color: theme.fg, + overflow: 'hidden', + backdropFilter: `blur(${bgBlur}px)`, + display: 'grid', + }), + body: css({ + borderLeft: `solid 1px ${Color.alpha(theme.fg, 0.1)}`, + padding: 10, + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.body.class}>{`š· Sidebar`}</div> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.tsx new file mode 100644 index 0000000000..676cb6d13a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/ui.tsx @@ -0,0 +1,127 @@ +import React, { useRef } from 'react'; +import { Player, VimeoBackground } from '../common.ts'; +import { LogoCanvas } from '../ui.Logo.Canvas/mod.ts'; + +import { type t, Color, css, DEFAULTS, LogoWordmark, Signal } from './common.ts'; +import { Layout } from './ui.Layout.tsx'; +import { Sidebar } from './ui.Sidebar.tsx'; +import { useKeyboard } from './use.Keyboard.ts'; +import { useSelectedPanel } from './use.SelectedPanel.ts'; + +type P = t.Landing1Props; +const D = DEFAULTS; + +const signalsFactory = () => + Player.Video.signals({ + // src: 'vimeo/727951677', // Rowan: "group scale", + src: 'vimeo/1068502644', // Trailer + }); + +/** + * Component: + */ +export const Landing: React.FC<P> = (props) => { + const { signals } = props; + const p = signals?.props; + const canvasPosition = p?.canvasPosition.value ?? D.canvasPosition; + const sidebarVisible = p?.sidebarVisible.value ?? D.sidebarVisible; + + /** + * Hooks: + */ + useKeyboard(); + const selectedPanel = useSelectedPanel(); + const playerSignalsRef = useRef(signalsFactory()); + const player = playerSignalsRef.current; + + /** + * Effects: + */ + Signal.useRedrawEffect(() => { + if (!p) return; + p.ready.value; + p.canvasPosition.value; + selectedPanel.value; + }); + + Signal.useEffect(() => { + const canvasPosition = p?.canvasPosition.value ?? DEFAULTS.canvasPosition; + if (!player) return; + if (canvasPosition === 'Center:Bottom') { + player.play(); + } else { + player.pause(); + } + }); + + /** + * Render. + */ + const sidebarRightWidth = sidebarVisible ? 360 : 0; + const theme = Color.theme(props.theme ?? 'Dark'); + const speed = 100; + + const styles = { + base: css({ + position: 'relative', + color: theme.fg, + backgroundColor: theme.bg, + fontFamily: 'sans-serif', + }), + + fill: css({ Absolute: 0 }), + logo: css({ + Absolute: [null, 15, 12, null], + width: 110, + }), + + body: css({ Absolute: 0, display: 'grid' }), + layout: css({ + Absolute: [0, sidebarRightWidth, 0, 0], + transition: `right ${speed}ms ease-in-out`, + }), + sidebar: { + right: css({ + Absolute: [0, 0, 0, null], + width: sidebarRightWidth, + transition: `width ${speed}ms ease-in-out`, + }), + }, + }; + + const elLayout = ( + <Layout + style={styles.layout} + theme={theme.name} + canvas={{ + element: <LogoCanvas theme={theme.name} selected={selectedPanel.value} />, + position: p?.canvasPosition.value, + }} + video={{ + element: <Player.Video.View signals={playerSignalsRef.current} />, + }} + /> + ); + + const elSidebarRight = ( + <Sidebar style={styles.sidebar.right} theme={props.theme} width={sidebarRightWidth} /> + ); + + return ( + <div className={css(styles.base, props.style).class}> + <LogoWordmark style={styles.logo} theme={theme.name} /> + + <VimeoBackground + video={499921561} // Tubes. + opacity={canvasPosition === 'Center' ? 0.3 : 0.15} + theme={theme.name} + style={styles.fill} + /> + + <div className={styles.body.class}> + {elLayout} + {elSidebarRight} + </div> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/use.Keyboard.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/use.Keyboard.ts new file mode 100644 index 0000000000..92702a18e2 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/use.Keyboard.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { Keyboard, rx } from './common.ts'; + +export function useKeyboard() { + useEffect(() => { + const life = rx.disposable(); + const keyboard = Keyboard.until(life.dispose$); + + keyboard.on('Enter', () => { + const s = window.location.search; + const isDev = s.includes('dev=') || s.includes('d='); + if (!isDev) window.location.search = '?d'; + }); + + keyboard.on('Space', () => { + /** + * TODO š· + */ + console.log('š· START/STOP player'); + }); + + return life.dispose; + }); +} diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-1/use.SelectedPanel.ts b/deploy/@tdb.slc/src/ui/ui.Landing-1/use.SelectedPanel.ts new file mode 100644 index 0000000000..27b1f61aa0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-1/use.SelectedPanel.ts @@ -0,0 +1,28 @@ +import React, { useEffect, useState, useRef, useCallback } from 'react'; +import { type t, Signal, rx, CanvasPanel, Time } from './common.ts'; + +export function useSelectedPanel() { + const panel = Signal.useSignal<t.CanvasPanel>('purpose'); + + /** + * Effect: Cycle the selected SLC panel. + */ + useEffect(() => { + const delay = 2_000; + const life = rx.lifecycle(); + + const next = async () => { + if (life.disposed) return; + Signal.cycle(panel, CanvasPanel.list); + Time.delay(delay, next); + }; + + Time.delay(delay, next); + return life.dispose; + }, []); + + /** + * API + */ + return panel; +} diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-2/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-2/-SPEC.Debug.tsx new file mode 100644 index 0000000000..79b9df4483 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-2/-SPEC.Debug.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { type t, Button, Color, css, Signal } from './common.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + type P = t.Landing2Props; + const s = Signal.create; + const props = { + debug: s<P['debug']>(false), + theme: s<P['theme']>('Dark'), + backgroundVideo: s<P['backgroundVideo']>(0.15), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.debug.value; + p.backgroundVideo.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => { + debug.listen(); + }); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.title.class}>{'Landing-2'}</div> + <hr /> + + <Button block label={`debug: ${p.debug}`} onClick={() => Signal.toggle(p.debug)} /> + + <Button + block + label={`theme: "${p.theme}"`} + onClick={() => Signal.cycle<t.CommonTheme>(p.theme, ['Light', 'Dark'])} + /> + + <hr /> + + <Button + block + label={`backgroundVideo: ${p.backgroundVideo}`} + onClick={() => Signal.cycle(p.backgroundVideo, [undefined, 0, 0.15, 0.3, 0.5])} + /> + + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-2/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-2/-SPEC.tsx new file mode 100644 index 0000000000..9977744c58 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-2/-SPEC.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { createDebugSignals, Debug } from './-SPEC.Debug.tsx'; +import { Landing } from './mod.ts'; + +export default Spec.describe('Landing-2', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.host.tracelineColor(p.theme.value === 'Dark' ? 0.15 : -0.06); + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => ( + <Landing + theme={p.theme.value} + debug={p.debug.value} + backgroundVideo={p.backgroundVideo.value} + /> + )); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-2/common.ts b/deploy/@tdb.slc/src/ui/ui.Landing-2/common.ts new file mode 100644 index 0000000000..630470ac2c --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-2/common.ts @@ -0,0 +1,6 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-2/mod.ts b/deploy/@tdb.slc/src/ui/ui.Landing-2/mod.ts new file mode 100644 index 0000000000..f817f74f44 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-2/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { Landing } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-2/t.ts b/deploy/@tdb.slc/src/ui/ui.Landing-2/t.ts new file mode 100644 index 0000000000..041a6b5b70 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-2/t.ts @@ -0,0 +1,11 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type Landing2Props = { + debug?: boolean; + backgroundVideo?: t.Percent; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-2/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-2/ui.tsx new file mode 100644 index 0000000000..bcf86202f8 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-2/ui.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { type t, Color, css, VimeoBackground } from './common.ts'; + +import { LogoCanvas } from '../ui.Logo.Canvas/mod.ts'; +import { useKeyboard } from '../ui.Landing-1/use.Keyboard.ts'; +import { LogoWordmark } from '../ui.Logo.Wordmark/mod.ts'; + +export const Landing: React.FC<t.Landing2Props> = (props) => { + const { debug = false, backgroundVideo = 0 } = props; + + useKeyboard(); + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + position: 'relative', + containerType: 'size', + color: theme.fg, + backgroundColor: theme.bg, + }), + background: css({ Absolute: 0 }), + body: css({ + Absolute: 0, + overflow: 'hidden', + backgroundColor: debug ? 'rgba(255, 0, 0, 0.1)' /* RED:DEBUG */ : '', + display: 'grid', + }), + + canvas: { + base: css({ + backgroundColor: debug ? 'rgba(255, 0, 0, 0.1)' /* RED:DEBUG */ : '', + display: 'grid', + placeItems: 'center', + }) + .container('max-width: 550px', { PaddingX: 75 }) + .container('max-width: 320px', { PaddingX: 50 }).done, + subject: css({ + width: 400, + marginBottom: '6%', + }).container('max-width: 550px', { width: '100%' }).done, + }, + + logo: css({ + Absolute: [null, 10, 15, null], + width: 150, + transition: 'width 200ms', + }) + .container('max-width: 640px', { width: 110 }) + .container('max-height: 470px', { display: 'none' }).done, + }; + + const elBackground = backgroundVideo !== undefined && ( + <VimeoBackground + video={499921561} // Tubes. + opacity={backgroundVideo} + theme={theme.name} + style={styles.background} + /> + ); + + const elCanvas = ( + <div className={styles.canvas.base.class}> + <div className={styles.canvas.subject.class}> + <LogoCanvas theme={theme.name} /> + </div> + </div> + ); + + const elLogo = <LogoWordmark theme={theme.name} style={styles.logo} />; + const elBody = <div className={styles.body.class}>{elCanvas}</div>; + + return ( + <div className={css(styles.base, props.style).class}> + {elLogo} + {elBackground} + {elBody} + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-3/-SPEC.Debug.tsx new file mode 100644 index 0000000000..a549a4ea3e --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/-SPEC.Debug.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Content } from '../../ui.Content/mod.ts'; +import { pushStackContentButtons, screenBreakpointButton } from '../ui.Layout/-SPEC.tsx'; +import { type t, App, Button, css, ObjectView, Signal, Str } from './common.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = Awaited<ReturnType<typeof createDebugSignals>>; + +/** + * Signals: + */ +export async function createDebugSignals(init?: (e: DebugSignals) => void) { + type P = t.Landing3Props; + const s = Signal.create; + + const app = App.signals(); + const props = { debug: s<P['debug']>(true) }; + const api = { app, props }; + app.stack.push(await Content.factory('Entry')); + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const app = debug.app; + const p = app.props; + const d = debug.props; + + Signal.useRedrawEffect(() => { + d.debug.value; + app.listen(); + }); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.title.class}>{'Landing-3'}</div> + <Button block label={`debug: ${d.debug}`} onClick={() => Signal.toggle(d.debug)} /> + <hr /> + {screenBreakpointButton(app)} + <Button + block + label={`background.video.opacity: ${p.background.video.opacity}`} + onClick={() => Signal.cycle(p.background.video.opacity, [undefined, 0.15, 0.3, 1])} + /> + + <hr /> + {pushStackContentButtons(app)} + + <hr /> + <ObjectView block name={'dist'} data={wrangle.dist(app)} expand={1} /> + <ObjectView name={'stack'} data={app.stack.items} expand={1} style={{ marginTop: 10 }} /> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + dist(app: t.AppSignals) { + const dist = app.props.dist.value; + if (!dist) return { err: '[app.props.dist] not found' }; + return { + 'dist:size': Str.bytes(dist.size.bytes), + 'dist:hash:sha256': `#${dist.hash.digest.slice(-5)}`, + }; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-3/-SPEC.tsx new file mode 100644 index 0000000000..38ea9893b0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/-SPEC.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Signal, Spec } from '../-test.ui.ts'; +import { createDebugSignals, Debug } from './-SPEC.Debug.tsx'; +import { css, App } from './common.ts'; +import { Landing } from './mod.ts'; +import { updateForBreakpointSize } from '../ui.Layout/-SPEC.tsx'; + +export default Spec.describe('Landing-3', async (e) => { + const debug = await createDebugSignals(); + const app = debug.app; + const d = debug.props; + const p = app.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + const update = { size: () => updateForBreakpointSize(ctx, app) }; + + Signal.effect(() => { + d.debug.value; + app.listen(); + update.size(); + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => { + const styles = { + base: css({ + position: 'relative', + display: 'grid', + overflow: 'hidden', + }), + }; + + return ( + <div className={styles.base.class}> + <Landing state={app} debug={d.debug.value} /> + </div> + ); + }); + + update.size(); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/common.ts b/deploy/@tdb.slc/src/ui/ui.Landing-3/common.ts new file mode 100644 index 0000000000..45d99b6304 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/common.ts @@ -0,0 +1,16 @@ +export * from '../common.ts'; + +/** + * Libs: + */ +export { App } from '../App/mod.ts'; +export { Layout } from '../ui.Layout/mod.ts'; +export { VideoBackground } from '../ui.Video.Background/mod.ts'; +export { useKeyboard } from '../use/mod.ts'; + +export { Content } from '../../ui.Content/mod.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/mod.ts b/deploy/@tdb.slc/src/ui/ui.Landing-3/mod.ts new file mode 100644 index 0000000000..b5d5b9266b --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/mod.ts @@ -0,0 +1,5 @@ +/** + * @module + */ +export { App, Content } from './common.ts'; +export { Landing } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/t.ts b/deploy/@tdb.slc/src/ui/ui.Landing-3/t.ts new file mode 100644 index 0000000000..c82ef31fd0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/t.ts @@ -0,0 +1,10 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type Landing3Props = { + state?: t.AppSignals; + debug?: boolean; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/ui.Content.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-3/ui.Content.tsx new file mode 100644 index 0000000000..7cec6ce743 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/ui.Content.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { type t, css, Layout } from './common.ts'; + +export type ContentProps = { + state?: t.AppSignals; + breakpoint: t.Breakpoint; + style?: t.CssInput; +}; + +type P = ContentProps; + +/** + * Component: + */ +export const Content: React.FC<P> = (props) => { + const { breakpoint, state } = props; + + /** + * Render: + */ + const styles = { + base: css({ display: 'grid' }), + }; + + const className = css(styles.base, props.style).class; + const el = Layout.render(breakpoint, state); + return <div className={className}>{el}</div>; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Landing-3/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Landing-3/ui.tsx new file mode 100644 index 0000000000..3bc6c9ad37 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Landing-3/ui.tsx @@ -0,0 +1,105 @@ +import React, { useEffect } from 'react'; +import { + type t, + App, + Color, + css, + Layout, + pkg, + rx, + useDist, + useSizeObserver, + VideoBackground, +} from './common.ts'; +import { Content } from './ui.Content.tsx'; + +type P = t.Landing3Props; + +/** + * Component: + */ +export const Landing: React.FC<P> = (props) => { + const { debug, state } = props; + const p = state?.props; + + const size = useSizeObserver(); + const width = size.rect?.width ?? -1; + const breakpoint = Layout.Breakpoint.fromWidth(width); + + const isReady = size.ready && breakpoint.is.ready; + + /** + * Hooks: + */ + const dist = useDist({ + useSampleFallback: wrangle.showSampleDist(props), + }); + + /** + * Effect: + */ + useEffect(() => { + const life = rx.lifecycle(); + if (state) App.Signals.Controllers.start(state, life); + return life.dispose; + }, [state]); + + /** + * Effects: + */ + useEffect(() => { + console.info(`š¦ ${pkg.name}@${pkg.version}: dist.json ā`, dist.json); + if (state) state.props.dist.value = dist.json; + }, [dist.count, !!state]); + + useEffect(() => { + if (!p) return; + p.screen.breakpoint.value = breakpoint.name; + }, [breakpoint.name]); + + if (!p) return; + const bg = p.background; + const backgroundVideoOpacity = bg.video.opacity.value; + + /** + * Render: + */ + const theme = Color.theme('Dark'); + const styles = { + base: css({ + backgroundColor: theme.bg, + color: theme.fg, + fontFamily: 'sans-serif', + opacity: isReady ? 1 : 0, + }), + fill: css({ Absolute: 0, display: 'grid' }), + }; + + const elBackground = typeof backgroundVideoOpacity === 'number' && ( + <VideoBackground state={state} style={styles.fill} /> + ); + + const elBody = ( + <div ref={size.ref} className={styles.fill.class}> + <Content breakpoint={breakpoint} state={state} /> + </div> + ); + + return ( + <div className={css(styles.base, props.style).class}> + {elBackground} + {elBody} + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + showSampleDist(props: P) { + const { debug } = props; + const isLocalhost = location.hostname === 'localhost'; + return debug ?? (isLocalhost && location.port !== '8080'); + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.Debug.tsx new file mode 100644 index 0000000000..5f914d4d29 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.Debug.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Sample } from './-SPEC.u.sample.tsx'; +import { + Content, + layerVideoPlayerButtons, + pushStackContentButtons, + screenBreakpointButton, +} from './-SPEC.u.tsx'; +import { type t, App, Button, css, Signal, Str, ObjectView } from './common.ts'; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = Awaited<ReturnType<typeof createDebugSignals>>; + +/** + * Signals: + */ +export async function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const app = App.signals(); + + const props = { theme: s<t.CommonTheme>('Dark') }; + const api = { + app, + props, + listen() { + app.listen(); + props.theme.value; + }, + }; + + // app.props.screen.breakpoint.value = 'Mobile'; + app.props.screen.breakpoint.value = 'Desktop'; + + app.stack.push(Sample.sample0()); + app.stack.push(await Content.factory('Entry')); + // app.stack.push(await Content.factory('Overview')); + + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const app = debug.app; + const p = app.props; + const d = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + const pushSample = (name: string, fn: () => t.Content) => { + return <Button block label={`stack.push:( "${name}" )`} onClick={() => app.stack.push(fn())} />; + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.title.class}>{`${p.screen.breakpoint.value} Layout`}</div> + + <Button + block + label={`theme: ${d.theme}`} + onClick={() => Signal.cycle<t.CommonTheme>(d.theme, ['Light', 'Dark'])} + /> + + <hr /> + {screenBreakpointButton(app)} + + <hr /> + <div className={css(styles.title, styles.cols).class}> + <div>{`Stack:`}</div> + <div /> + <div>{`${app.stack.length} ${Str.plural(app.stack.length, 'Layer', 'Layers')}`}</div> + </div> + + {pushSample('sample-layer-0', Sample.sample0)} + {pushSample('sample-layer-1', Sample.sample1)} + {pushSample('sample: top-down', Sample.sample2)} + + <hr /> + {pushStackContentButtons(app)} + + <hr /> + {layerVideoPlayerButtons(app)} + + <hr /> + <div className={styles.title.class}>{`Sample Configurations:`}</div> + + <Button + block + label={() => `- debug samples`} + onClick={() => { + app.stack.clear(); + app.stack.push(Sample.sample0()); + }} + /> + <Button + block + label={() => `- application ("SLC Product System")`} + onClick={async () => { + app.stack.clear(); + app.stack.push(await Content.factory('Entry')); + }} + /> + + <hr /> + <ObjectView name={'stack'} data={app.stack.items} expand={1} /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.tsx new file mode 100644 index 0000000000..7073a38db5 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.tsx @@ -0,0 +1,46 @@ +import { type t, Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { updateForBreakpointSize, Content } from './-SPEC.u.tsx'; +import { Color, css, App } from './common.ts'; +import { Layout } from './m.Layout.tsx'; + +export * from './-SPEC.u.tsx'; + +export default Spec.describe('MobileLayout', async (e) => { + const debug = await createDebugSignals(); + const app = debug.app; + const p = app.props; + + e.it('init', async (e) => { + const ctx = Spec.ctx(e); + const update = { size: () => updateForBreakpointSize(ctx, app) }; + + Dev.Theme.signalEffect(ctx, debug.props.theme, 1); + Signal.effect(() => { + debug.listen(); + update.size(); + ctx.redraw(); + }); + + ctx.host.tracelineColor(Color.alpha(Color.CYAN, 0.3)); + ctx.subject + .size() + .display('grid') + .render((e) => { + const style = css({ display: 'grid', overflow: 'hidden' }); + const el = Layout.render(p.screen.breakpoint.value, debug.app); + return <div className={style.class}>{el}</div>; + }); + + /** + * Initialize environment. + */ + update.size(); + await App.Render.preload(debug.app, Content.factory, 'Entry', 'Trailer'); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.u.sample.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.u.sample.tsx new file mode 100644 index 0000000000..4a58e88266 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.u.sample.tsx @@ -0,0 +1,84 @@ +import { type t, css, SheetBase } from './common.ts'; + +export const Sample = { + sample0(): t.Content { + const id = 'sample-0'; + return { + id, + render(props) { + const { state } = props; + const styles = { + base: css({ + pointerEvents: 'auto', + cursor: 'default', + userSelect: 'none', + opacity: 0.3, + padding: 6, + }), + }; + + return ( + <div + className={styles.base.class} + onDoubleClick={() => state.stack.clear(1)} + onClick={() => { + if (!props.is.top) state.stack.pop(1); + }} + > + <div>{`id: "${id}"`}</div> + <div>{'props.children š·'}</div> + </div> + ); + }, + }; + }, + + sample1(): t.Content { + const id = 'sample-1'; + return { + id, + render(props) { + const { state } = props; + const edge: t.SheetMarginInput = + state.breakpoint.name === 'Desktop' ? ['1fr', 390, '1fr'] : 10; + + const onClick = () => { + if (!props.is.top) props.state.stack.pop(); + }; + + const styles = { base: css({ padding: 10 }) }; + + return ( + <SheetBase.View edgeMargin={edge} onClick={onClick}> + <div className={styles.base.class}> + <div>{`š Hello: "${id}" [${props.index}]`}</div> + <div>{'props.children š·'}</div> + </div> + </SheetBase.View> + ); + }, + }; + }, + + sample2(): t.Content { + return { + id: 'sample-2', + render(props) { + const { state } = props; + const breakpoint = state.breakpoint; + const orientation: t.SheetOrientation = 'Top:Down'; + const edge: t.SheetMarginInput = breakpoint.name === 'Desktop' ? [30, '1fr', 30] : 10; + const styles = { base: css({ padding: 10 }) }; + + return ( + <SheetBase.View orientation={orientation} edgeMargin={edge}> + <div className={styles.base.class}> + <div>{`š Hello: "${props.content.id}" [${props.index}]`}</div> + <div>{'props.children š·'}</div> + </div> + </SheetBase.View> + ); + }, + }; + }, +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.u.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.u.tsx new file mode 100644 index 0000000000..3347f9df48 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/-SPEC.u.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Content } from '../../ui.Content/mod.ts'; +import { type t, Button, Signal } from './common.ts'; + +export { Content }; + +/** + * Used to update a size of a DevHost subject based on the current size-breakpoint. + */ +export const updateForBreakpointSize = (dev: t.DevCtx, app: t.AppSignals) => { + const breakpoint = app.props.screen.breakpoint.value; + if (breakpoint === 'Mobile') dev.subject.size([390, 844]); + if (breakpoint === 'Intermediate') dev.subject.size([600, 650]); + if (breakpoint === 'Desktop') dev.subject.size('fill'); +}; + +/** + * Button: push content onto the stack. + */ +export const pushStackButton = (app: t.AppSignals, stage: t.ContentStage) => { + return ( + <Button + key={`stack.${stage}`} + block + label={`stack.push:( "${stage}" )`} + onClick={async () => app.stack.push(await Content.factory(stage))} + /> + ); +}; + +/** + * Button Set: content pushing and clearing from the stack. + */ +export const pushStackContentButtons = (app: t.AppSignals) => { + const clear = (leave: number) => { + return ( + <Button + block + key={`stack.clear(${leave ?? 0})`} + label={`stack.clear${leave > 0 ? `( leave: ${leave} )` : ''}`} + onClick={() => app.stack.clear(leave)} + /> + ); + }; + + return [ + pushStackButton(app, 'Entry'), + pushStackButton(app, 'Trailer'), + pushStackButton(app, 'Overview'), + pushStackButton(app, 'Programme'), + <React.Fragment key={'hr:pop'}> + <hr /> + <Button block label={`stack.pop`} onClick={() => app.stack.pop()} /> + </React.Fragment>, + clear(1), + clear(0), + ]; +}; + +/** + * Button: cycle through screen breakpoints. + */ +export const screenBreakpointButton = (app: t.AppSignals) => { + type T = t.BreakpointName; + const p = app.props; + const list: T[] = ['Desktop', 'Intermediate', 'Mobile']; + return ( + <Button + block + label={`screen.breakpoint: ${p.screen.breakpoint ?? '<undefined>'}`} + onClick={() => Signal.cycle<T>(p.screen.breakpoint, list)} + /> + ); +}; + +/** + * Buttons: play/pause controls for media-player signals-API on the stack. + */ +export function layerVideoPlayerButtons(app: t.AppSignals) { + const videoLayers = app.stack.items.filter((layer) => Content.Is.video(layer)); + if (videoLayers.length === 0) return <div>{`(no video layers)`}</div>; + + return videoLayers.map((layer, index) => { + const p = layer.video?.props; + if (!p) return null; + return ( + <Button + block + key={`${layer.id}.${index}`} + label={() => `[ ${layer.id} ]: playing: ${p.playing.value}`} + onClick={() => Signal.toggle(p.playing)} + subscribe={() => p.playing.value} + /> + ); + }); +} diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/common.ts b/deploy/@tdb.slc/src/ui/ui.Layout/common.ts new file mode 100644 index 0000000000..601c981ebf --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/common.ts @@ -0,0 +1,12 @@ +export { Breakpoint } from '../App.Layout/mod.ts'; +export { App } from '../App/mod.ts'; +export { LogoWordmark } from '../ui.Logo.Wordmark/mod.ts'; +export { Sheet } from '../ui.Sheet/mod.ts'; +export { TooSmall } from '../ui.TooSmall/mod.ts'; + +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/m.Layout.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/m.Layout.tsx new file mode 100644 index 0000000000..a4b31c2b50 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/m.Layout.tsx @@ -0,0 +1,28 @@ +import { type t, Breakpoint } from './common.ts'; + +import { Layout as Base } from '../App.Layout/mod.ts'; +import { LayoutDesktop } from './ui.Desktop.tsx'; +import { LayoutIntermediate } from './ui.Intermediate.tsx'; +import { LayoutMobile } from './ui.Mobile.tsx'; + +/** + * Main Layout API (with UI components). + */ +export const Layout = { + ...Base, + + /** + * Render factory for the <Layout> component that matches the current size-breakpoint. + */ + render(size: t.BreakpointSizeInput, state?: t.AppSignals) { + const theme: t.CommonTheme = 'Dark'; + const breakpoint = Breakpoint.from(size); + const is = breakpoint.is; + + if (is.mobile) return <LayoutMobile state={state} theme={theme} />; + if (is.intermediate) return <LayoutIntermediate state={state} theme={theme} />; + if (is.desktop) return <LayoutDesktop state={state} theme={theme} />; + + return <div>{`Unsupported supported breakpoint: "${breakpoint.name}"`}</div>; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/mod.ts b/deploy/@tdb.slc/src/ui/ui.Layout/mod.ts new file mode 100644 index 0000000000..055d3e1b99 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/mod.ts @@ -0,0 +1,8 @@ +/** + * @module + */ +export { Layout } from './m.Layout.tsx'; + +export { LayoutDesktop } from './ui.Desktop.tsx'; +export { LayoutIntermediate } from './ui.Intermediate.tsx'; +export { LayoutMobile } from './ui.Mobile.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/t.ts b/deploy/@tdb.slc/src/ui/ui.Layout/t.ts new file mode 100644 index 0000000000..e9e41a68e3 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/t.ts @@ -0,0 +1,28 @@ +import type { t } from './common.ts'; + +/** + * <Component>: Mobile + */ +export type LayoutMobileProps = { + state?: t.AppSignals; + style?: t.CssInput; + theme?: t.CommonTheme; +}; + +/** + * <Component>: Intermediate + */ +export type LayoutIntermediateProps = { + state?: t.AppSignals; + style?: t.CssInput; + theme?: t.CommonTheme; +}; + +/** + * <Component>: Desktop + */ +export type LayoutDesktopProps = { + state?: t.AppSignals; + style?: t.CssInput; + theme?: t.CommonTheme; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/ui.Desktop.Footer.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Desktop.Footer.tsx new file mode 100644 index 0000000000..6576727902 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Desktop.Footer.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { type t, Button, Color, css, LogoWordmark } from './common.ts'; + +export type DesktopFooterProps = { + state?: t.AppSignals; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +type P = DesktopFooterProps; + +/** + * Component: + */ +export const DesktopFooter: React.FC<P> = (props) => { + const { state } = props; + const p = state?.props; + if (!p) return null; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + fontSize: 11, + padding: 10, + color: theme.fg, + display: 'grid', + gridTemplateColumns: `auto 1fr auto`, + pointerEvents: 'none', + }), + left: css({ + pointerEvents: 'auto', + display: 'grid', + gridAutoFlow: 'column', + alignContent: 'end', + columnGap: '10px', + }), + right: css({ pointerEvents: 'auto' }), + logo: { cc: { width: 100, marginRight: 4 } }, + }; + + const elDiv = <div>{'ā¢'}</div>; + + const dist = p.dist.value; + const elDist = dist && ( + <Button + theme={theme.name} + label={() => `version: #${wrangle.versionHash(state)}`} + onClick={() => window.open('./dist.json', '_blank')} + /> + ); + + const elPdfDownload = ( + <Button + theme={theme.name} + label={() => `pdf worksheet`} + onClick={() => window.open('./pdf/slc.pdf', '_blank')} + /> + ); + + const elLogos = <LogoWordmark logo={'CC'} theme={theme.name} style={styles.logo.cc} />; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.left.class}> + {elDist} + {elDiv} + {elPdfDownload} + </div> + <div /> + <div className={styles.right.class}>{elLogos}</div> + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + versionHash(state: t.AppSignals) { + const dist = state.props.dist.value; + const hx = dist?.hash.digest ?? '000000'; + return hx.slice(-5); + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/ui.Desktop.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Desktop.tsx new file mode 100644 index 0000000000..e415f4d31b --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Desktop.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { type t, App, Cropmarks, css, Signal, TooSmall, useSizeObserver } from './common.ts'; +import { DesktopFooter } from './ui.Desktop.Footer.tsx'; + +type P = t.LayoutDesktopProps; + +/** + * Component: + */ +export const LayoutDesktop: React.FC<P> = (props) => { + const { state } = props; + const p = state?.props; + if (!p) return null; + + const size = useSizeObserver(); + const isReady = size.ready; + let isTooSmall = wrangle.tooSmall(size.rect); + + /** + * Effects: + */ + Signal.useRedrawEffect(() => state.listen()); + + /** + * Render: + */ + const styles = { + base: css({ + opacity: isReady ? 1 : 0, + position: 'relative', + display: 'grid', + }), + bg: css({ Absolute: 0, display: 'grid' }), + body: css({ Absolute: 0, display: 'grid' }), + stack: css({ Absolute: 0, display: 'grid', pointerEvents: 'none' }), + cropmarks: { + base: css({ Absolute: 0, display: 'grid', pointerEvents: 'none' }), + body: css({ width: 390, pointerEvents: 'auto' }), + }, + footer: css({ Absolute: [null, 0, 0, 0] }), + }; + + const elCropmarks = ( + <div className={styles.cropmarks.base.class}> + <Cropmarks + theme={'Dark'} + borderOpacity={0.05} + size={{ mode: 'fill', x: false, y: true, margin: [0, 40, 0, 40] }} + > + <div className={styles.cropmarks.body.class}></div> + </Cropmarks> + </div> + ); + + const elStackItems = App.Render.stack(state); + const elStack = <div className={styles.stack.class}>{elStackItems}</div>; + const elFooter = <DesktopFooter theme={props.theme} style={styles.footer} state={state} />; + + const elBody = ( + <React.Fragment> + <div className={styles.bg.class} /> + <div className={styles.body.class}> + {elCropmarks} + {elStack} + </div> + {elFooter} + </React.Fragment> + ); + + const elTooSmall = <TooSmall theme={props.theme} />; + + return ( + <div ref={size.ref} className={css(styles.base, props.style).class}> + {isTooSmall ? elTooSmall : elBody} + </div> + ); +}; + +/** + * Helpers + */ +const wrangle = { + tooSmall(size?: t.DomRect) { + if (!size) return null; + if (size.height < 520) return true; + if (size.width < 630) return true; + return false; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/ui.Intermediate.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Intermediate.tsx new file mode 100644 index 0000000000..ad4f7f7f41 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Intermediate.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { type t, Cropmarks, css, TooSmall } from './common.ts'; + +type P = t.LayoutIntermediateProps; + +/** + * Component: + */ +export const LayoutIntermediate: React.FC<P> = (props) => { + const { state } = props; + const p = state?.props; + if (!p) return null; + + /** + * Render: + */ + const styles = { + base: css({ position: 'relative', userSelect: 'none', display: 'grid' }), + body: css({}), + }; + + const msg = ` + Please make your window bigger, or + move over to your mobile device. + `; + + return ( + <div className={css(styles.base, props.style).class}> + <Cropmarks theme={'Dark'} borderOpacity={0.05}> + <div className={styles.body.class}> + <TooSmall theme={props.theme}>{msg}</TooSmall> + </div> + </Cropmarks> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Layout/ui.Mobile.tsx b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Mobile.tsx new file mode 100644 index 0000000000..4e440e93e6 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Layout/ui.Mobile.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { type t, App, css, Signal } from './common.ts'; + +type P = t.LayoutMobileProps; + +export const LayoutMobile: React.FC<P> = (props) => { + const { state } = props; + const p = state?.props; + + Signal.useRedrawEffect(() => { + state?.listen(); + }); + if (!p) return null; + + /** + * Render: + */ + const styles = { + base: css({ position: 'relative', display: 'grid' }), + }; + + /** + * The stack of sheets. + */ + const elStack = App.Render.stack(state); + return <div className={css(styles.base, props.style).class}>{elStack}</div>; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/-SPEC.Debug.tsx new file mode 100644 index 0000000000..230ba6a9d2 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/-SPEC.Debug.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { type t, Button, CanvasPanel, Color, css, Signal, D } from './common.ts'; +import { Selection } from './m.Selection.ts'; + +/** + * Types + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssValue }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = t.LogoCanvasProps; + +/** + * Signals + */ +export function createDebugSignals() { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Dark'), + width: s<P['width']>(400), + over: s<P['over']>(), + selected: s<P['selected']>('purpose'), + selectionAnimation: s<P['selectionAnimation']>(), + }; + const api = { + props, + listen() { + const p = props; + p.width.value; + p.over.value; + p.theme.value; + p.selected.value; + p.selectionAnimation.value; + }, + }; + return api; +} + +/** + * Component + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle(p.theme, ['Light', 'Dark'])} + /> + <Button + block={true} + label={`width: ${p.width.value ?? '<undefined>'}`} + onClick={() => Signal.cycle(p.width, [undefined, 280, 400])} + /> + + <hr /> + {canvasSelectedButton(p.selected)} + + <Button + block={true} + label={() => { + const value = Selection.animation(p.selectionAnimation.value); + return `selectionAnimation.loop: ${value.loop ?? '<undefined>'}`; + }} + onClick={() => { + const next = Selection.animation(p.selectionAnimation.value); + next.loop = !(next.loop ?? D.selectionAnimation.loop); + p.selectionAnimation.value = next; + }} + /> + </div> + ); +}; + +/** + * Dev: selected panel(s) test button. + */ +export function canvasSelectedButton(signal: t.Signal<P['selected']>) { + return ( + <Button + block={true} + label={() => { + const value = signal.value; + const fmt = Array.isArray(value) ? `array[${value.length}]` : value ?? '<undefined>'; + return `selected: ${fmt}`; + }} + onClick={() => Signal.cycle(signal, [undefined, CanvasPanel.list, 'purpose'])} + /> + ); +} diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/-SPEC.tsx new file mode 100644 index 0000000000..58384e43eb --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/-SPEC.tsx @@ -0,0 +1,65 @@ +import { type t, Dev, Spec } from '../-test.ui.ts'; +import { createDebugSignals, Debug } from './-SPEC.Debug.tsx'; +import { css, Signal } from './common.ts'; +import { LogoCanvas } from './mod.ts'; + +export default Spec.describe('Logo.Canvas', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + const renderCanvas = (options: { theme?: t.CommonTheme } = {}) => { + return ( + <LogoCanvas + theme={options.theme ?? p.theme.value} + width={p.width.value} + selected={p.selected.value} + selectionAnimation={p.selectionAnimation.value} + over={p.over.value} + onPanelEvent={(e) => { + if (e.type === 'leave' && p.over.value === e.panel) p.over.value = undefined; + if (e.type === 'enter') p.over.value = e.panel; + if (e.type === 'click') p.selected.value = e.panel; + }} + /> + ); + }; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + const updateSize = () => { + const width = p.width.value; + if (width === undefined) ctx.subject.size('fill-x', 180); + else ctx.subject.size([width, null]); + }; + + Dev.Theme.signalEffect(ctx, debug.props.theme, 1); + Signal.effect(() => { + debug.listen(); + updateSize(); + ctx.redraw(); + }); + + ctx.subject.display('grid').render((e) => renderCanvas()); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); + + e.it('ui:footer', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.footer.render((e) => { + const styles = { + base: css({ display: 'grid', placeItems: 'center', marginBottom: 55 }), + body: css({ width: 220 }), + }; + return ( + <div className={styles.base.class}> + <div className={styles.body.class}>{renderCanvas({ theme: 'Light' })}</div> + </div> + ); + }); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/canvas.mini.svg b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/canvas.mini.svg new file mode 100644 index 0000000000..366d200007 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/canvas.mini.svg @@ -0,0 +1,37 @@ +<svg width="354" height="184" viewBox="0 0 354 184" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="canvas.mini" clip-path="url(#clip0_1_15)"> +<g id="panels"> +<path id="panel.impact" opacity="0.3" d="M350.697 22.1289C350.697 11.6355 342.19 3.12891 331.697 3.12891H177V38.0391H350.697V22.1289Z" fill="white"/> +<rect id="panel.problem" opacity="0.3" x="2.39258" y="37.8516" width="73.4749" height="106.705" fill="white"/> +<rect id="panel.solution" opacity="0.3" x="75.8672" y="37.5206" width="67.7266" height="51.2861" fill="white"/> +<rect id="panel.metrics" opacity="0.3" x="75.8672" y="88.2812" width="67.7266" height="56.4634" fill="white"/> +<rect id="panel.uvp" opacity="0.3" x="143.594" y="37.8516" width="70.5621" height="106.705" fill="white"/> +<rect id="panel.advantage" opacity="0.3" x="214.156" y="37.8516" width="67.5486" height="50.9541" fill="white"/> +<rect id="panel.channels" opacity="0.3" x="214.156" y="88.4758" width="67.5482" height="56.0801" fill="white"/> +<rect id="panel.customers" opacity="0.3" x="281.704" y="38.0391" width="69.5714" height="106.068" fill="white"/> +<path id="panel.costs" opacity="0.3" d="M3.30273 161.718C3.30273 172.212 11.8093 180.718 22.3027 180.718H176.999V144.557H3.30273V161.718Z" fill="white"/> +<path id="panel.revenue" opacity="0.3" d="M351.276 161.719C351.276 172.212 342.77 180.719 332.276 180.719H176.999V144.745H351.276V161.719Z" fill="white"/> +<path id="panel.purpose" opacity="0.3" d="M3.30365 22.1289C3.30365 11.6355 11.8102 3.12891 22.3036 3.12891H177V38.0391H3.30365V22.1289Z" fill="white"/> +</g> +<g id="outline"> +<rect id="border" x="3" y="3" width="348.248" height="177.711" rx="17" fill="white" fill-opacity="0.1" stroke="white" stroke-width="6"/> +<g id="grid-lines"> +<line id="Line 445" x1="5.28125" y1="38.0703" x2="348.718" y2="38.0703" stroke="white" stroke-width="6"/> +<line id="Line 446" x1="5.28125" y1="144.795" x2="348.718" y2="144.795" stroke="white" stroke-width="6"/> +<line id="Line 447" x1="75.9106" y1="37.8496" x2="75.9106" y2="144.555" stroke="white" stroke-width="6"/> +<line id="Line 448" x1="143.303" y1="37.8496" x2="143.303" y2="144.555" stroke="white" stroke-width="6"/> +<line id="Line 451" x1="78.5269" y1="88.5293" x2="142.72" y2="88.5293" stroke="white" stroke-width="6"/> +<line id="Line 447_2" x1="213.896" y1="37.8496" x2="213.896" y2="144.555" stroke="white" stroke-width="6"/> +<line id="Line 448_2" x1="281.288" y1="37.8496" x2="281.288" y2="144.555" stroke="white" stroke-width="6"/> +<line id="Line 451_2" x1="216.512" y1="88.5293" x2="280.704" y2="88.5293" stroke="white" stroke-width="6"/> +<line id="Line 454" x1="176.904" y1="144.795" x2="176.904" y2="180.716" stroke="white" stroke-width="6"/> +<line id="Line 455" x1="176.904" y1="3.28125" x2="176.904" y2="39.2026" stroke="white" stroke-width="6"/> +</g> +</g> +</g> +<defs> +<clipPath id="clip0_1_15"> +<rect width="354" height="184" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/common.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/common.ts new file mode 100644 index 0000000000..15284cc8e2 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/common.ts @@ -0,0 +1,12 @@ +import type { t } from '../common.ts'; +export * from '../common.ts'; + +const selectionAnimation: Required<t.LogoCanvasSelectionAnimation> = { + loop: false, + delay: 200, +}; + +export const DEFAULTS = { + selectionAnimation, +} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/m.Selection.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/m.Selection.ts new file mode 100644 index 0000000000..50931dae43 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/m.Selection.ts @@ -0,0 +1,18 @@ +import { type t, D } from './common.ts'; + +type P = t.LogoCanvasProps; + +/** + * Helpers for working with the component selection properties. + */ +export const Selection = { + selected(value: P['selected']): t.CanvasPanel[] { + if (!value) return []; + return Array.isArray(value) ? value : [value]; + }, + + animation(value: P['selectionAnimation']): t.LogoCanvasSelectionAnimation { + const DEFAULT = D.selectionAnimation; + return value ? { ...DEFAULT, ...value } : DEFAULT; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/mod.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/mod.ts new file mode 100644 index 0000000000..2297de7c69 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/mod.ts @@ -0,0 +1,6 @@ +/** + * @module + */ +export { Theme } from './u.ts'; +export { LogoCanvas } from './ui.tsx'; +export { Selection } from './m.Selection.ts'; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/t.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/t.ts new file mode 100644 index 0000000000..48065de60d --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/t.ts @@ -0,0 +1,30 @@ +import type { t } from './common.ts'; + +/** + * <Component>: Mini Canvas + */ +export type LogoCanvasProps = { + selected?: t.CanvasPanel | t.CanvasPanel[]; + selectionAnimation?: LogoCanvasSelectionAnimation; + over?: t.CanvasPanel; + width?: number; + theme?: t.CommonTheme; + style?: t.CssInput; + onPanelEvent?: t.LogoCanvasPanelHandler; +}; + +/** Selection animation options */ +export type LogoCanvasSelectionAnimation = { + /** Cycle back to the beginning when animation completes. */ + loop?: boolean; + delay?: t.Msecs; +}; + +/** + * Events + */ +export type LogoCanvasPanelHandler = (e: LogoCanvasPanelHandlerArgs) => void; +export type LogoCanvasPanelHandlerArgs = { + type: 'enter' | 'leave' | 'click'; + panel: t.CanvasPanel; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/u.Theme.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/u.Theme.ts new file mode 100644 index 0000000000..fddd40477b --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/u.Theme.ts @@ -0,0 +1,7 @@ +import { type t, Color } from './common.ts'; + +export const Theme = { + color(theme: t.CommonTheme = 'Light') { + return theme === 'Dark' ? Color.WHITE : Color.DARK; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/u.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/u.ts new file mode 100644 index 0000000000..8bc5696ae3 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/u.ts @@ -0,0 +1 @@ +export * from './u.Theme.ts'; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/ui.Svg.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/ui.Svg.tsx new file mode 100644 index 0000000000..01a840ab3a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/ui.Svg.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { type t, Color, css, Svg } from './common.ts'; +import { useMouse } from './use.Mouse.tsx'; +import { useTheme } from './use.Theme.ts'; + +export type SvgImageProps = { + width?: number; + selected?: t.CanvasPanel; + over?: t.CanvasPanel; + bgBlur?: number; + + theme?: t.CommonTheme; + style?: t.CssInput; + + onPanelEvent?: t.LogoCanvasPanelHandler; +}; + +/** + * Component. + * + */ +export const SvgImage: React.FC<SvgImageProps> = (props) => { + const { width, bgBlur = 20, over, selected, onPanelEvent } = props; + const theme = Color.theme(props.theme); + + /** + * Source design, search Figma: "canvas.mini" + */ + const svg = Svg.useSvg<HTMLDivElement>(() => import('./canvas.mini.svg'), [354, 184]); + + useTheme(svg, theme.name); + useMouse(svg, { theme: theme.name, over, selected, onPanelEvent }); + + /** + * Render: + */ + const styles = { + base: css({ + position: 'relative', + cursor: 'default', + color: theme.fg, + backdropFilter: `blur(${bgBlur}px)`, + lineHeight: 0, // NB: ensure no "baseline" gap below the <svg>. + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div ref={svg.ref} /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/ui.tsx new file mode 100644 index 0000000000..cc5fd3a199 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/ui.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { type t, css } from './common.ts'; +import { SvgImage } from './ui.Svg.tsx'; +import { useSelection } from './use.Selection.ts'; + +type P = t.LogoCanvasProps; + +/** + * Component. + */ +export const LogoCanvas: React.FC<P> = (props) => { + const { over, onPanelEvent, width } = props; + const { selected } = useSelection(props); + + /** + * Render: + */ + const styles = { + base: css({ + position: 'relative', + cursor: 'default', + userSelect: 'none', + }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <SvgImage + theme={props.theme} + width={width} + selected={selected} + over={over} + onPanelEvent={onPanelEvent} + /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Mouse.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Mouse.tsx new file mode 100644 index 0000000000..8bf8a70eab --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Mouse.tsx @@ -0,0 +1,70 @@ +import type { t } from './common.ts'; + +import { useEffect } from 'react'; +import { Theme } from './u.ts'; + +type Options = { + selected?: t.CanvasPanel; + over?: t.CanvasPanel; + theme?: t.CommonTheme; + onPanelEvent?: t.LogoCanvasPanelHandler; +}; + +/** + * Manages mouse behavior on a Canvas. + */ +export function useMouse<T extends HTMLElement>(svg: t.SvgInstance<T>, options: Options = {}) { + useCanvasPanelMouse('purpose', svg, options); + useCanvasPanelMouse('customers', svg, options); + useCanvasPanelMouse('problem', svg, options); + useCanvasPanelMouse('uvp', svg, options); + useCanvasPanelMouse('solution', svg, options); + useCanvasPanelMouse('channels', svg, options); + useCanvasPanelMouse('revenue', svg, options); + useCanvasPanelMouse('costs', svg, options); + useCanvasPanelMouse('metrics', svg, options); + useCanvasPanelMouse('advantage', svg, options); + useCanvasPanelMouse('impact', svg, options); +} + +/** + * Manages mouse behavior on an individual Canvas panel. + */ +export function useCanvasPanelMouse<T extends HTMLElement>( + panel: t.CanvasPanel, + svg: t.SvgInstance<T>, + options: Options = {}, +) { + const { selected, over, theme, onPanelEvent } = options; + const isSelected = selected === panel; + + useEffect(() => { + if (!svg.ready) return; + + const query = `#panel\\.${panel}`; + const svgPanel = svg.query(query); + const color = Theme.color(theme); + + const updateOpacity = () => { + const isOver = over === panel; + const opacity = isSelected ? 1 : isOver ? 0.2 : 0; + svgPanel?.opacity(opacity); + svgPanel?.fill(color); + }; + + svg.query('#outline')?.css('pointer-events' as any, 'none'); // NB: Allow click-through of the grid lines that sit above each panel. + updateOpacity(); // Set default opacity. + + /** + * Event Handlers. + */ + const onOver = (isOver: boolean) => { + updateOpacity(); + onPanelEvent?.({ panel, type: isOver ? 'enter' : 'leave' }); + }; + svgPanel?.off(); + svgPanel?.on('mouseover', () => onOver(true)); + svgPanel?.on('mouseleave', () => onOver(false)); + svgPanel?.on('mousedown', () => onPanelEvent?.({ panel, type: 'click' })); + }, [svg.ready, panel, selected, over, theme, isSelected]); +} diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Selection.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Selection.ts new file mode 100644 index 0000000000..4ac0aca876 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Selection.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; +import { type t, Time } from './common.ts'; +import { Selection } from './m.Selection.ts'; + +type P = t.LogoCanvasProps; + +export function useSelection(props: t.LogoCanvasProps) { + const animation = Selection.animation(props.selectionAnimation); + const [selected, setSeleted] = useState<t.CanvasPanel>(); + + /** + * Effect: keep selected state up-to-date. + */ + useEffect(() => { + const time = Time.until(); + + async function animateSelected(list: t.CanvasPanel[]) { + if (time.disposed) return; + for (const value of list) { + if (time.disposed) break; + setSeleted(value); + await time.wait(animation.delay); + } + if (animation.loop) animateSelected(list); // ā š³ RECURSION. + } + + if (Array.isArray(props.selected)) { + animateSelected(props.selected); + } else { + setSeleted(props.selected); + } + + return time.dispose; + }, [ + // Deps: + Selection.selected(props.selected).join(':'), + animation.delay, + animation.loop, + ]); + + /** + * API: + */ + return { selected } as const; +} diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Theme.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Theme.ts new file mode 100644 index 0000000000..19f984a575 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Canvas/use.Theme.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import { type t } from './common.ts'; +import { Theme } from './u.ts'; + +/** + * Handle updating the canvas theme. + */ +export function useTheme(svg: t.SvgInstance<HTMLDivElement>, theme?: t.CommonTheme) { + useEffect(() => { + const color = Theme.color(theme); + const setColor = (color: string, elements: t.SvgElement[]) => { + elements.forEach((el) => { + el?.fill(color); + el?.stroke(color); + }); + }; + setColor(color, svg.queryAll('#border')); + setColor(color, svg.queryAll('#outline')); + setColor(color, svg.queryAll('#grid-lines line')); + }, [svg.draw, theme]); +} diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/-SPEC.Debug.tsx new file mode 100644 index 0000000000..e971e25b2a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/-SPEC.Debug.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { type t, Button, Color, css, DEFAULTS, Signal } from './common.ts'; + +const D = DEFAULTS; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; +type P = DebugProps; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + type P = t.LogoWordmarkProps; + const props = { + width: s<number | undefined>(300), + theme: s<P['theme']>('Dark'), + logo: s<P['logo']>(D.logo), + }; + const api = { + props, + listen() { + const p = props; + p.width.value; + p.theme.value; + p.logo.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<P> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const theme = Color.theme(p.theme.value); + const styles = { + base: css({ color: theme.fg }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`theme: ${p.theme}`} + onClick={() => Signal.cycle<t.LogoWordmarkProps['theme']>(p.theme, ['Light', 'Dark'])} + /> + <Button + block + label={`width: ${p.width.value ?? '<undefined>'}`} + onClick={() => Signal.cycle(p.width, [undefined, 90, 150, 300])} + /> + + <hr /> + <Button + block + label={`logo: "${p.logo}"`} + onClick={() => Signal.cycle<t.LogoKind>(p.logo, ['SLC', 'CC'])} + /> + + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/-SPEC.tsx new file mode 100644 index 0000000000..0fab3af041 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/-SPEC.tsx @@ -0,0 +1,37 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { LogoWordmark } from './mod.ts'; + +export default Spec.describe('Logo.Wordmark', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + const updateSize = () => { + const width = p.width.value; + if (width === undefined) ctx.subject.size('fill-x', 180); + else ctx.subject.size([width, null]); + }; + + Dev.Theme.signalEffect(ctx, p.theme); + Signal.effect(() => { + debug.listen(); + updateSize(); + ctx.redraw(); + }); + + ctx.subject + .size() + .display('grid') + .render((e) => <LogoWordmark theme={p.theme.value} logo={p.logo.value} />); + + updateSize(); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/common.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/common.ts new file mode 100644 index 0000000000..f26e229f95 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/common.ts @@ -0,0 +1,15 @@ +import { type t } from '../common.ts'; + +export * from '../common.ts'; +export { Theme } from '../ui.Logo.Canvas/mod.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { + width: 90, + get logo(): t.LogoKind { + return 'SLC'; + }, +} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/images/logo.cc.svg b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/images/logo.cc.svg new file mode 100644 index 0000000000..445c14b0c1 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/images/logo.cc.svg @@ -0,0 +1,20 @@ +<svg width="345" height="84" viewBox="0 0 345 84" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M34.9299 81.8755C54.2212 81.8755 69.8598 66.2368 69.8598 46.9456C69.8598 27.6543 54.2212 12.0157 34.9299 12.0157C15.6386 12.0157 0 27.6543 0 46.9456C0 66.2368 15.6386 81.8755 34.9299 81.8755ZM34.6505 75.7278C50.7008 75.7278 63.7121 62.7165 63.7121 46.6661C63.7121 30.6158 50.7008 17.6045 34.6505 17.6045C18.6001 17.6045 5.58878 30.6158 5.58878 46.6661C5.58878 62.7165 18.6001 75.7278 34.6505 75.7278Z" fill="black"/> +<path d="M272.967 45.9334C267.765 45.9334 263.592 44.3393 260.446 41.1513C257.341 37.9212 255.789 33.8103 255.789 28.8185C255.789 23.9525 257.32 19.9045 260.383 16.6745C263.487 13.4444 267.472 11.8294 272.338 11.8294C276.742 11.8294 280.413 13.1718 283.349 15.8565C287.209 19.3801 289.096 24.6027 289.012 31.5241H265.857C266.192 33.7474 266.968 35.4883 268.185 36.7467C269.401 37.9632 271.037 38.5714 273.093 38.5714C275.736 38.5714 277.518 37.5018 278.441 35.3624H288.446C287.691 38.4666 285.887 41.0044 283.035 42.976C280.224 44.9476 276.868 45.9334 272.967 45.9334ZM265.92 25.4207H278.63C278.462 23.4911 277.812 21.9599 276.679 20.8273C275.589 19.6947 274.205 19.1284 272.527 19.1284C268.877 19.1284 266.675 21.2258 265.92 25.4207Z" fill="black"/> +<path d="M235.85 44.9895L224.713 12.7733H235.284L239.689 27.3083L241.45 33.7893H241.576C242.122 31.608 242.709 29.4477 243.338 27.3083L247.806 12.7733H258.062L246.925 44.9895H235.85Z" fill="black"/> +<path d="M214.066 44.9895V12.7732H224.322V44.9895H214.066ZM214.066 8.30575V0H224.322V8.30575H214.066Z" fill="black"/> +<path d="M206.667 45.367C203.395 45.367 200.857 44.7168 199.053 43.4165C197.292 42.0741 196.411 39.767 196.411 36.495V19.1913H192.132V12.7733H196.411V2.57983H206.415V12.7733H212.267V19.1913H206.415V34.2927C206.415 35.4673 206.709 36.2853 207.296 36.7467C207.926 37.2081 208.827 37.4388 210.002 37.4388C210.338 37.4388 210.778 37.4388 211.323 37.4388C211.869 37.3969 212.183 37.3759 212.267 37.3759V44.8637C211.093 45.1993 209.226 45.367 206.667 45.367Z" fill="black"/> +<path d="M171.855 45.8075C168.541 45.8075 165.878 44.9476 163.864 43.2277C161.851 41.5078 160.844 39.0958 160.844 35.9916C160.844 32.7196 161.934 30.3496 164.116 28.8814C166.297 27.3712 169.317 26.3645 173.177 25.8611C176.323 25.4836 178.462 25.0641 179.595 24.6027C180.769 24.0993 181.357 23.3232 181.357 22.2745C181.357 19.9674 179.888 18.8138 176.952 18.8138C173.806 18.8138 172.065 20.1981 171.729 22.9667H162.291C162.417 19.7786 163.717 17.1359 166.192 15.0385C168.709 12.941 172.275 11.8923 176.889 11.8923C181.503 11.8923 184.985 12.7732 187.334 14.5351C190.061 16.5066 191.424 19.5689 191.424 23.7217V39.9557C191.424 42.5146 191.802 44.0457 192.557 44.549V44.9895H182.615C182.196 44.4022 181.839 43.2067 181.545 41.4029H181.419C179.364 44.3393 176.176 45.8075 171.855 45.8075ZM175.127 39.2636C177.057 39.2636 178.609 38.7392 179.783 37.6905C181 36.5999 181.608 35.1946 181.608 33.4747V29.6364C180.601 30.1398 178.84 30.7061 176.323 31.3353C174.267 31.7968 172.82 32.3421 171.981 32.9713C171.142 33.6006 170.723 34.5024 170.723 35.677C170.723 38.068 172.191 39.2636 175.127 39.2636Z" fill="black"/> +<path d="M144.763 45.9334C139.561 45.9334 135.387 44.3393 132.241 41.1513C129.137 37.9212 127.585 33.8103 127.585 28.8185C127.585 23.9525 129.116 19.9045 132.178 16.6745C135.282 13.4444 139.268 11.8294 144.134 11.8294C148.538 11.8294 152.209 13.1718 155.145 15.8565C159.004 19.3801 160.892 24.6027 160.808 31.5241H137.653C137.988 33.7474 138.764 35.4883 139.981 36.7467C141.197 37.9632 142.833 38.5714 144.889 38.5714C147.531 38.5714 149.314 37.5018 150.237 35.3624H160.242C159.487 38.4666 157.683 41.0044 154.83 42.976C152.02 44.9476 148.664 45.9334 144.763 45.9334ZM137.715 25.4207H150.426C150.258 23.4911 149.608 21.9599 148.475 20.8273C147.385 19.6947 146 19.1284 144.322 19.1284C140.673 19.1284 138.471 21.2258 137.715 25.4207Z" fill="black"/> +<path d="M107.447 44.9895V12.7732H117.263V17.807H117.451C119.633 14.1156 122.569 12.2699 126.261 12.2699C127.225 12.2699 127.897 12.3538 128.274 12.5216V21.3307H128.022C124.834 20.8693 122.317 21.4356 120.472 23.0296C118.626 24.5817 117.703 27.0776 117.703 30.5174V44.9895H107.447Z" fill="black"/> +<path d="M90.7759 45.9963C85.6582 45.9963 81.5263 44.3813 78.3801 41.1512C75.234 37.9212 73.661 33.8522 73.661 28.9443C73.661 24.0364 75.2131 19.9674 78.3172 16.7374C81.4214 13.5073 85.4904 11.8923 90.5242 11.8923C94.8029 11.8923 98.3056 13.0249 101.032 15.2901C103.759 17.5553 105.353 20.5337 105.814 24.2251H95.9355C95.2224 21.0371 93.4605 19.443 90.65 19.443C88.4687 19.443 86.8117 20.282 85.6791 21.9599C84.5885 23.6378 84.0432 25.966 84.0432 28.9443C84.0432 31.8807 84.6095 34.1878 85.7421 35.8658C86.8747 37.5017 88.5106 38.3197 90.65 38.3197C93.922 38.3197 95.7887 36.474 96.2501 32.7826H106.066C105.94 36.5999 104.472 39.7669 101.661 42.2838C98.8928 44.7588 95.2643 45.9963 90.7759 45.9963Z" fill="black"/> +<path d="M329.865 84C325.166 84 321.412 82.9932 318.602 80.9797C315.833 78.9243 314.365 76.1137 314.197 72.5481H323.887C324.265 75.7362 326.236 77.3302 329.802 77.3302C331.354 77.3302 332.57 77.0576 333.451 76.5122C334.374 75.925 334.836 75.128 334.836 74.1212C334.836 73.7856 334.794 73.492 334.71 73.2403C334.626 72.9886 334.479 72.7579 334.269 72.5481C334.059 72.3384 333.85 72.1496 333.64 71.9818C333.472 71.814 333.2 71.6672 332.822 71.5414C332.444 71.3736 332.109 71.2477 331.815 71.1638C331.564 71.08 331.165 70.9961 330.62 70.9122C330.116 70.7863 329.718 70.7024 329.424 70.6605C329.131 70.5766 328.669 70.4927 328.04 70.4088C327.411 70.2829 326.949 70.199 326.656 70.1571C325.313 69.9473 324.181 69.7376 323.258 69.5279C322.335 69.2762 321.307 68.8986 320.175 68.3953C319.084 67.8919 318.182 67.3256 317.469 66.6964C316.798 66.0252 316.231 65.1443 315.77 64.0536C315.309 62.921 315.078 61.6206 315.078 60.1524C315.078 56.8805 316.399 54.3426 319.042 52.5388C321.685 50.735 324.999 49.8332 328.984 49.8332C333.472 49.8332 336.954 50.756 339.429 52.6017C341.904 54.4055 343.267 56.9224 343.519 60.1524H334.08C333.745 57.6355 332.025 56.3771 328.921 56.3771C327.578 56.3771 326.488 56.6288 325.649 57.1322C324.852 57.6355 324.453 58.3696 324.453 59.3344C324.453 59.5861 324.495 59.8168 324.579 60.0266C324.663 60.2363 324.789 60.4251 324.957 60.5929C325.166 60.7607 325.355 60.9075 325.523 61.0333C325.733 61.1592 326.005 61.285 326.341 61.4109C326.677 61.5367 326.97 61.6416 327.222 61.7255C327.516 61.8094 327.893 61.8933 328.354 61.9772C328.858 62.0611 329.256 62.145 329.55 62.2289C329.886 62.2708 330.326 62.3547 330.871 62.4806C331.417 62.5645 331.857 62.6274 332.193 62.6693C333.619 62.921 334.794 63.1517 335.716 63.3615C336.639 63.5712 337.709 63.9487 338.925 64.4941C340.184 64.9975 341.17 65.6057 341.883 66.3188C342.596 66.99 343.204 67.9338 343.708 69.1503C344.253 70.3249 344.526 71.7092 344.526 73.3032C344.526 76.743 343.162 79.3857 340.436 81.2314C337.751 83.0771 334.227 84 329.865 84Z" fill="black"/> +<path d="M282.15 82.9933V50.777H292.029V55.1816H292.217C294.692 51.616 298.006 49.8332 302.159 49.8332C305.641 49.8332 308.367 50.9658 310.339 53.231C312.353 55.4542 313.359 58.3906 313.359 62.0401V82.9933H303.103V64.1165C303.103 62.3547 302.663 60.9704 301.782 59.9637C300.943 58.915 299.705 58.3906 298.069 58.3906C296.391 58.3906 295.028 59.0198 293.979 60.2783C292.931 61.5367 292.406 63.1727 292.406 65.1862V82.9933H282.15Z" fill="black"/> +<path d="M258.296 74.0582C259.47 75.8201 261.148 76.701 263.33 76.701C265.511 76.701 267.189 75.8201 268.363 74.0582C269.58 72.2964 270.188 69.9263 270.188 66.948C270.188 63.9697 269.58 61.5996 268.363 59.8378C267.189 58.034 265.511 57.1321 263.33 57.1321C261.148 57.1321 259.47 58.013 258.296 59.7749C257.121 61.5367 256.534 63.9277 256.534 66.948C256.534 69.9263 257.121 72.2964 258.296 74.0582ZM275.788 79.155C272.642 82.385 268.51 84 263.393 84C258.275 84 254.122 82.385 250.934 79.155C247.746 75.9249 246.152 71.856 246.152 66.948C246.152 62.0401 247.746 57.9711 250.934 54.7411C254.122 51.5111 258.275 49.8961 263.393 49.8961C268.51 49.8961 272.642 51.5111 275.788 54.7411C278.976 57.9711 280.57 62.0401 280.57 66.948C280.57 71.856 278.976 75.9249 275.788 79.155Z" fill="black"/> +<path d="M194.798 82.9933V50.777H204.614V55.3703H204.803C207.026 51.6789 210.277 49.8332 214.556 49.8332C218.793 49.8332 221.813 51.7208 223.617 55.4962H223.743C226.176 51.7208 229.552 49.8332 233.873 49.8332C237.439 49.8332 240.144 50.9448 241.99 53.168C243.878 55.3494 244.822 58.3067 244.822 62.0401V82.9933H234.565V63.8648C234.565 60.2154 233.055 58.3906 230.035 58.3906C228.441 58.3906 227.182 58.9989 226.259 60.2154C225.379 61.3899 224.938 62.963 224.938 64.9345V82.9933H214.682V63.8648C214.682 60.2154 213.172 58.3906 210.151 58.3906C208.557 58.3906 207.299 58.9989 206.376 60.2154C205.495 61.3899 205.055 62.963 205.055 64.9345V82.9933H194.798Z" fill="black"/> +<path d="M141.998 82.9933V50.777H151.813V55.3703H152.002C154.226 51.6789 157.477 49.8332 161.755 49.8332C165.992 49.8332 169.012 51.7208 170.816 55.4962H170.942C173.375 51.7208 176.752 49.8332 181.072 49.8332C184.638 49.8332 187.344 50.9448 189.189 53.168C191.077 55.3494 192.021 58.3067 192.021 62.0401V82.9933H181.765V63.8648C181.765 60.2154 180.254 58.3906 177.234 58.3906C175.64 58.3906 174.382 58.9989 173.459 60.2154C172.578 61.3899 172.137 62.963 172.137 64.9345V82.9933H161.881V63.8648C161.881 60.2154 160.371 58.3906 157.351 58.3906C155.757 58.3906 154.498 58.9989 153.575 60.2154C152.694 61.3899 152.254 62.963 152.254 64.9345V82.9933H141.998Z" fill="black"/> +<path d="M118.144 74.0582C119.318 75.8201 120.996 76.701 123.177 76.701C125.359 76.701 127.037 75.8201 128.211 74.0582C129.428 72.2964 130.036 69.9263 130.036 66.948C130.036 63.9697 129.428 61.5996 128.211 59.8378C127.037 58.034 125.359 57.1321 123.177 57.1321C120.996 57.1321 119.318 58.013 118.144 59.7749C116.969 61.5367 116.382 63.9277 116.382 66.948C116.382 69.9263 116.969 72.2964 118.144 74.0582ZM135.636 79.155C132.49 82.385 128.358 84 123.24 84C118.123 84 113.97 82.385 110.782 79.155C107.594 75.9249 106 71.856 106 66.948C106 62.0401 107.594 57.9711 110.782 54.7411C113.97 51.5111 118.123 49.8961 123.24 49.8961C128.358 49.8961 132.49 51.5111 135.636 54.7411C138.824 57.9711 140.418 62.0401 140.418 66.948C140.418 71.856 138.824 75.9249 135.636 79.155Z" fill="black"/> +<path d="M90.7759 84C85.6582 84 81.5263 82.385 78.3801 79.155C75.234 75.9249 73.661 71.856 73.661 66.948C73.661 62.0401 75.2131 57.9711 78.3172 54.7411C81.4214 51.5111 85.4904 49.8961 90.5242 49.8961C94.8029 49.8961 98.3056 51.0287 101.032 53.2939C103.759 55.5591 105.353 58.5374 105.814 62.2288H95.9355C95.2224 59.0408 93.4605 57.4467 90.65 57.4467C88.4687 57.4467 86.8117 58.2857 85.6791 59.9636C84.5885 61.6416 84.0432 63.9697 84.0432 66.948C84.0432 69.8844 84.6095 72.1916 85.7421 73.8695C86.8747 75.5055 88.5106 76.3235 90.65 76.3235C93.922 76.3235 95.7887 74.4777 96.2501 70.7863H106.066C105.94 74.6036 104.472 77.7707 101.661 80.2876C98.8928 82.7625 95.2643 84 90.7759 84Z" fill="black"/> +<path d="M45.1031 57.4654C41.8891 57.4654 39.2942 56.4511 37.3184 54.4226C35.3426 52.3941 34.3546 49.8387 34.3546 46.7564C34.3546 43.6741 35.3294 41.1187 37.2789 39.0902C39.2283 37.0617 41.7837 36.0474 44.9451 36.0474C47.6322 36.0474 49.8319 36.7587 51.5443 38.1813C53.2567 39.6039 54.2578 41.4744 54.5476 43.7927H48.3435C47.8956 41.7905 46.7892 40.7894 45.0241 40.7894C43.6542 40.7894 42.6136 41.3163 41.9023 42.3701C41.2173 43.4238 40.8749 44.8859 40.8749 46.7564C40.8749 48.6005 41.2305 50.0494 41.9418 51.1032C42.6531 52.1306 43.6805 52.6443 45.0241 52.6443C47.0789 52.6443 48.2513 51.4852 48.5411 49.1669H54.7056C54.6266 51.5642 53.7045 53.5532 51.9395 55.1339C50.2007 56.6882 47.922 57.4654 45.1031 57.4654Z" fill="black"/> +<path d="M24.7938 57.4654C21.5798 57.4654 18.9849 56.4511 17.0091 54.4226C15.0333 52.3941 14.0453 49.8387 14.0453 46.7564C14.0453 43.6741 15.0201 41.1187 16.9696 39.0902C18.919 37.0617 21.4744 36.0475 24.6358 36.0475C27.3229 36.0475 29.5226 36.7588 31.235 38.1813C32.9474 39.6039 33.9485 41.4744 34.2383 43.7927H28.0342C27.5863 41.7905 26.4799 40.7894 24.7148 40.7894C23.3449 40.7894 22.3043 41.3163 21.593 42.3701C20.908 43.4239 20.5656 44.886 20.5656 46.7564C20.5656 48.6005 20.9212 50.0495 21.6325 51.1032C22.3438 52.1307 23.3712 52.6444 24.7148 52.6444C26.7697 52.6444 27.942 51.4852 28.2318 49.1669H34.3963C34.3173 51.5643 33.3952 53.5532 31.6302 55.1339C29.8915 56.6882 27.6127 57.4654 24.7938 57.4654Z" fill="black"/> +</svg> diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/images/logo.slc.svg b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/images/logo.slc.svg new file mode 100644 index 0000000000..3670945fa2 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/images/logo.slc.svg @@ -0,0 +1,26 @@ +<svg width="112" height="81" viewBox="0 0 112 81" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_722_5)"> +<path d="M82.3339 26.6294V1.64714H87.5356V26.6294H82.3339Z" fill="white"/> +<path d="M69.0968 26.9716C65.6404 26.9716 62.7999 24.9867 62.7999 21.3591V21.2907C62.7999 17.2867 65.8457 15.4387 70.1919 15.4387C72.0399 15.4387 73.3746 15.7467 74.675 16.1916V15.8836C74.675 13.7276 73.3404 12.5298 70.7395 12.5298C68.7546 12.5298 67.3515 12.9063 65.6746 13.5223L64.3742 9.55247C66.3933 8.6627 68.3781 8.08092 71.4924 8.08092C74.3328 8.08092 76.3862 8.83381 77.6866 10.1343C79.0555 11.5031 79.6715 13.5223 79.6715 15.9863V26.6294H74.6408V24.6445C73.3746 26.0476 71.6293 26.9716 69.0968 26.9716ZM70.671 23.3783C73.1008 23.3783 74.7435 22.0436 74.7435 20.1614V19.2374C73.8537 18.8267 72.6902 18.5529 71.4239 18.5529C69.1995 18.5529 67.8306 19.4427 67.8306 21.0854V21.1538C67.8306 22.5569 68.9941 23.3783 70.671 23.3783Z" fill="white"/> +<path d="M55.8611 26.6294V8.28625H61.0629V26.6294H55.8611Z" fill="white"/> +<path d="M46.7823 27.04C41.2726 27.04 37.2343 22.7965 37.2343 17.5605V17.492C37.2343 12.256 41.2383 7.94403 46.8508 7.94403C50.3072 7.94403 52.4632 9.10759 54.1743 11.024L50.9917 14.4463C49.8281 13.2143 48.6646 12.4271 46.8166 12.4271C44.2157 12.4271 42.3677 14.72 42.3677 17.4236V17.492C42.3677 20.2983 44.1814 22.5569 47.0219 22.5569C48.7672 22.5569 49.965 21.804 51.2312 20.6063L54.277 23.6863C52.4974 25.6369 50.4441 27.04 46.7823 27.04Z" fill="white"/> +<path d="M26.4522 27.04C20.7713 27.04 16.562 22.8307 16.562 17.5605V17.492C16.562 12.2218 20.8055 7.94403 26.5206 7.94403C32.2015 7.94403 36.4108 12.1534 36.4108 17.4236V17.492C36.4108 22.7623 32.1673 27.04 26.4522 27.04ZM26.5206 22.5569C29.4979 22.5569 31.2775 20.264 31.2775 17.5605V17.492C31.2775 14.7885 29.3268 12.4271 26.4522 12.4271C23.4748 12.4271 21.6953 14.72 21.6953 17.4236V17.492C21.6953 20.1956 23.646 22.5569 26.5206 22.5569Z" fill="white"/> +<path d="M8.7609 26.9716C6.19423 26.9716 3.31956 26.116 0.889786 24.1996L3.11423 20.7774C5.09912 22.2147 7.18667 22.9676 8.89779 22.9676C10.4036 22.9676 11.088 22.42 11.088 21.5987V21.5302C11.088 20.4009 9.30845 20.0245 7.28934 19.4085C4.72267 18.6556 1.81379 17.4578 1.81379 13.8987V13.8302C1.81379 10.1 4.82534 8.01247 8.52134 8.01247C10.8485 8.01247 13.3809 8.79958 15.3658 10.1342L13.3809 13.7276C11.5671 12.6667 9.75334 12.0165 8.41867 12.0165C7.15245 12.0165 6.50223 12.564 6.50223 13.2827V13.3511C6.50223 14.3778 8.24756 14.8569 10.2325 15.5414C12.7991 16.3969 15.7765 17.6289 15.7765 20.9827V21.0511C15.7765 25.1236 12.7307 26.9716 8.7609 26.9716Z" fill="white"/> +<path d="M47.7344 54.6294V36.2863H52.9362V38.8871C54.134 37.3471 55.674 35.944 58.3091 35.944C62.2446 35.944 64.5375 38.5449 64.5375 42.7543V54.6294H59.3357V44.3969C59.3357 41.9329 58.1722 40.6667 56.1873 40.6667C54.2024 40.6667 52.9362 41.9329 52.9362 44.3969V54.6294H47.7344Z" fill="white"/> +<path d="M34.7369 54.9716C31.2804 54.9716 28.44 52.9867 28.44 49.3592V49.2907C28.44 45.2867 31.4858 43.4387 35.832 43.4387C37.68 43.4387 39.0147 43.7467 40.3151 44.1916V43.8836C40.3151 41.7276 38.9804 40.5298 36.3796 40.5298C34.3947 40.5298 32.9916 40.9063 31.3147 41.5223L30.0142 37.5525C32.0333 36.6627 34.0182 36.0809 37.1324 36.0809C39.9729 36.0809 42.0262 36.8338 43.3267 38.1343C44.6956 39.5032 45.3116 41.5223 45.3116 43.9863V54.6294H40.2809V52.6445C39.0147 54.0476 37.2693 54.9716 34.7369 54.9716ZM36.3111 51.3783C38.7409 51.3783 40.3836 50.0436 40.3836 48.1614V47.2374C39.4938 46.8267 38.3302 46.5529 37.064 46.5529C34.8396 46.5529 33.4707 47.4427 33.4707 49.0854V49.1538C33.4707 50.5569 34.6342 51.3783 36.3111 51.3783Z" fill="white"/> +<path d="M19.2931 55.04C13.7833 55.04 9.71083 51.1729 9.71083 45.5605V45.492C9.71083 40.256 13.4411 35.944 18.7797 35.944C24.9055 35.944 27.7117 40.7009 27.7117 45.9027C27.7117 46.3134 27.6775 46.7925 27.6433 47.2716H14.8784C15.3917 49.6329 17.0344 50.8649 19.3615 50.8649C21.1068 50.8649 22.3731 50.3174 23.8104 48.9827L26.7877 51.6178C25.0766 53.7396 22.6126 55.04 19.2931 55.04ZM14.8099 44.0205H22.6468C22.3388 41.6934 20.9699 40.1191 18.7797 40.1191C16.6237 40.1191 15.2206 41.6591 14.8099 44.0205Z" fill="white"/> +<path d="M2.56667 54.6294V29.6472H7.76844V54.6294H2.56667Z" fill="white"/> +<path d="M98.8549 78.2614C96.2882 78.2614 93.4135 77.4059 90.9838 75.4894L93.2082 72.0672C95.1931 73.5045 97.2807 74.2574 98.9918 74.2574C100.498 74.2574 101.182 73.7099 101.182 72.8885V72.8201C101.182 71.6907 99.4024 71.3143 97.3833 70.6983C94.8167 69.9454 91.9078 68.7476 91.9078 65.1885V65.1201C91.9078 61.3899 94.9193 59.3023 98.6153 59.3023C100.942 59.3023 103.475 60.0894 105.46 61.4241L103.475 65.0174C101.661 63.9565 99.8473 63.3063 98.5127 63.3063C97.2464 63.3063 96.5962 63.8539 96.5962 64.5725V64.641C96.5962 65.6676 98.3415 66.1467 100.326 66.8312C102.893 67.6867 105.87 68.9187 105.87 72.2725V72.341C105.87 76.4134 102.825 78.2614 98.8549 78.2614Z" fill="white"/> +<path d="M79.4236 78.2614C75.9672 78.2614 73.1267 76.2765 73.1267 72.649V72.5805C73.1267 68.5765 76.1725 66.7285 80.5187 66.7285C82.3667 66.7285 83.7014 67.0365 85.0018 67.4814V67.1734C85.0018 65.0174 83.6672 63.8196 81.0663 63.8196C79.0814 63.8196 77.6783 64.1961 76.0014 64.8121L74.7009 60.8423C76.72 59.9525 78.7049 59.3707 81.8192 59.3707C84.6596 59.3707 86.7129 60.1236 88.0134 61.4241C89.3823 62.793 89.9983 64.8121 89.9983 67.2761V77.9192H84.9676V75.9343C83.7014 77.3374 81.956 78.2614 79.4236 78.2614ZM80.9978 74.6681C83.4276 74.6681 85.0703 73.3334 85.0703 71.4512V70.5272C84.1805 70.1165 83.0169 69.8427 81.7507 69.8427C79.5263 69.8427 78.1574 70.7325 78.1574 72.3752V72.4436C78.1574 73.8467 79.3209 74.6681 80.9978 74.6681Z" fill="white"/> +<path d="M62.2024 78.0561L54.9815 59.5761H60.4913L64.5979 71.8619L68.7388 59.5761H74.1459L66.925 78.0561H62.2024Z" fill="white"/> +<path d="M38.0238 77.9192V59.5761H43.2256V62.177C44.4234 60.637 45.9634 59.2339 48.5985 59.2339C52.5341 59.2339 54.8269 61.8347 54.8269 66.0441V77.9192H49.6252V67.6867C49.6252 65.2227 48.4616 63.9565 46.4767 63.9565C44.4918 63.9565 43.2256 65.2227 43.2256 67.6867V77.9192H38.0238Z" fill="white"/> +<path d="M25.0263 78.2614C21.5699 78.2614 18.7294 76.2765 18.7294 72.649V72.5805C18.7294 68.5765 21.7752 66.7285 26.1214 66.7285C27.9694 66.7285 29.3041 67.0365 30.6046 67.4814V67.1734C30.6046 65.0174 29.2699 63.8196 26.669 63.8196C24.6841 63.8196 23.281 64.1961 21.6041 64.8121L20.3037 60.8423C22.3228 59.9525 24.3077 59.3707 27.4219 59.3707C30.2623 59.3707 32.3157 60.1236 33.6161 61.4241C34.985 62.793 35.601 64.8121 35.601 67.2761V77.9192H30.5703V75.9343C29.3041 77.3374 27.5588 78.2614 25.0263 78.2614ZM26.6006 74.6681C29.0303 74.6681 30.673 73.3334 30.673 71.4512V70.5272C29.7832 70.1165 28.6197 69.8427 27.3534 69.8427C25.129 69.8427 23.7601 70.7325 23.7601 72.3752V72.4436C23.7601 73.8467 24.9237 74.6681 26.6006 74.6681Z" fill="white"/> +<path d="M10.9853 78.3299C5.47555 78.3299 1.43733 74.0863 1.43733 68.8503V68.7819C1.43733 63.5459 5.44133 59.2339 11.0538 59.2339C14.5102 59.2339 16.6662 60.3974 18.3773 62.3139L15.1947 65.7361C14.0311 64.5041 12.8676 63.717 11.0196 63.717C8.41866 63.717 6.57066 66.0099 6.57066 68.7134V68.7819C6.57066 71.5881 8.38444 73.8467 11.2249 73.8467C12.9702 73.8467 14.168 73.0939 15.4342 71.8961L18.48 74.9761C16.7004 76.9267 14.6471 78.3299 10.9853 78.3299Z" fill="white"/> +<ellipse cx="58.7434" cy="3.13287" rx="3.135" ry="3.13287" fill="white"/> +</g> +<defs> +<clipPath id="clip0_722_5"> +<rect width="112" height="80.6713" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/mod.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/mod.ts new file mode 100644 index 0000000000..9262c188b0 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { LogoWordmark } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/t.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/t.ts new file mode 100644 index 0000000000..8a9b1a415a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/t.ts @@ -0,0 +1,13 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type LogoWordmarkProps = { + logo?: t.LogoKind; + theme?: t.CommonTheme; + style?: t.CssInput; +}; + +/** List of supported logos. */ +export type LogoKind = 'SLC' | 'CC'; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/ui.tsx new file mode 100644 index 0000000000..e51a3a3ceb --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/ui.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { type t, css, Svg, D } from './common.ts'; +import { useTheme, setColors } from './use.Theme.ts'; + +const Images = { + slc: { import: () => import('./images/logo.slc.svg'), size: [112, 81] }, + cc: { import: () => import('./images/logo.cc.svg'), size: [345, 84] }, +}; + +export const LogoWordmark: React.FC<t.LogoWordmarkProps> = (props) => { + const { theme } = props; + const kind = props.logo ?? D.logo; + + /** + * Source design, search Figma: "logo.slc" + */ + const src = wrangle.svg(kind); + const svg = Svg.useSvg<HTMLDivElement>(src.import, src.size); + useTheme(svg, theme); + + /** + * Keep SVG colors in sync with the current props. + */ + React.useEffect(() => setColors(svg, theme), [kind, svg, theme]); + + /** + * Render: + */ + const styles = { base: css({}) }; + return ( + <div className={css(styles.base, props.style).class}> + <div ref={svg.ref} /> + </div> + ); +}; + +/** + * Helpers: + */ +const wrangle = { + svg(kind: 'SLC' | 'CC') { + if (kind === 'SLC') return Images.slc; + if (kind === 'CC') return Images.cc; + throw new Error(`Not supported: "${kind}"`); + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/use.Theme.ts b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/use.Theme.ts new file mode 100644 index 0000000000..e26e2c6579 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Logo.Wordmark/use.Theme.ts @@ -0,0 +1,18 @@ +import { useState, useEffect } from 'react'; +import { type t, Theme } from './common.ts'; + +/** + * Handle updating the canvas theme. + */ +export function useTheme(svg: t.SvgInstance<HTMLDivElement>, theme?: t.CommonTheme) { + useEffect(() => setColors(svg, theme), [svg.draw, theme]); +} + +export function setColors(svg: t.SvgInstance<HTMLDivElement>, theme?: t.CommonTheme) { + const setColor = (color: string, selector: string) => { + svg.queryAll(selector).forEach((el) => el.fill(color)); + }; + const color = Theme.color(theme); + setColor(color, 'path'); + setColor(color, 'ellipse'); +} diff --git a/deploy/@tdb.slc/src/ui/ui.Sheet/common.ts b/deploy/@tdb.slc/src/ui/ui.Sheet/common.ts new file mode 100644 index 0000000000..b6fd03f641 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Sheet/common.ts @@ -0,0 +1,2 @@ +export { Layout } from '../App.Layout/mod.ts'; +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui/ui.Sheet/mod.ts b/deploy/@tdb.slc/src/ui/ui.Sheet/mod.ts new file mode 100644 index 0000000000..8a882f83e7 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Sheet/mod.ts @@ -0,0 +1 @@ +export { Sheet } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.Sheet/t.ts b/deploy/@tdb.slc/src/ui/ui.Sheet/t.ts new file mode 100644 index 0000000000..f11b679830 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Sheet/t.ts @@ -0,0 +1,8 @@ +import type { SheetProps as BaseSheetProps } from '@sys/ui-react-components/t'; +import type { t } from './common.ts'; + +/** + * <Sheet> component props. + */ +export type SheetProps = t.ContentProps & BaseProps & { orientation?: t.SheetOrientationY }; +type BaseProps = Pick<BaseSheetProps, 'children' | 'edgeMargin'>; diff --git a/deploy/@tdb.slc/src/ui/ui.Sheet/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Sheet/ui.tsx new file mode 100644 index 0000000000..536f4db6b8 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Sheet/ui.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { type t, Color, css, SheetBase } from './common.ts'; + +/** + * Component: + */ +export const Sheet: React.FC<t.SheetProps> = (props) => { + const { state, is, index, orientation } = props; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({}), + body: css({ display: 'grid', pointerEvents: 'auto' }), + }; + + return ( + <SheetBase.View + style={css(styles.base, props.style)} + theme={theme.name} + edgeMargin={props.edgeMargin} + orientation={props.orientation} + > + <div className={styles.body.class}>{props.children}</div> + </SheetBase.View> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.TooSmall/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.TooSmall/-SPEC.Debug.tsx new file mode 100644 index 0000000000..370082ca32 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.TooSmall/-SPEC.Debug.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { type t, Button, css, Signal, Str } from './common.ts'; + +type P = t.TooSmallProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const props = { + theme: s<P['theme']>('Light'), + children: s<P['children']>(), + }; + const api = { + props, + listen() { + const p = props; + p.theme.value; + p.children.value; + }, + }; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const p = debug.props; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + title: css({ fontWeight: 'bold', marginBottom: 10 }), + cols: css({ display: 'grid', gridTemplateColumns: 'auto 1fr auto' }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={css(styles.title, styles.cols).class}> + <div>{'TooSmall'}</div> + </div> + <Button + block + label={() => `theme: ${p.theme}`} + onClick={() => Signal.cycle<P['theme']>(p.theme, ['Light', 'Dark'])} + /> + <Button + block + label={() => { + const value = p.children.value; + const fmt = value ? Str.truncate(String(value), 40) : '<undefined>'; + return `children: ${fmt}`; + }} + onClick={() => { + const multiline = ` + Please make your window bigger, or + move over to your mobile device. + `; + + Signal.cycle<P['children']>(p.children, [ + undefined, + 'š Hello', + multiline, + Str.Lorem.text, + ]); + }} + /> + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.TooSmall/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.TooSmall/-SPEC.tsx new file mode 100644 index 0000000000..4db0001b84 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.TooSmall/-SPEC.tsx @@ -0,0 +1,28 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { TooSmall } from './mod.ts'; + +export default Spec.describe('TooSmall', (e) => { + const debug = createDebugSignals(); + const p = debug.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, p.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size([390, null]) + .display('grid') + .render((e) => <TooSmall theme={p.theme.value}>{p.children.value}</TooSmall>); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.TooSmall/common.ts b/deploy/@tdb.slc/src/ui/ui.TooSmall/common.ts new file mode 100644 index 0000000000..a45006948c --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.TooSmall/common.ts @@ -0,0 +1,7 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = {} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui/ui.TooSmall/mod.ts b/deploy/@tdb.slc/src/ui/ui.TooSmall/mod.ts new file mode 100644 index 0000000000..6b25093f95 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.TooSmall/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { TooSmall } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.TooSmall/t.ts b/deploy/@tdb.slc/src/ui/ui.TooSmall/t.ts new file mode 100644 index 0000000000..9161f2588e --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.TooSmall/t.ts @@ -0,0 +1,10 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type TooSmallProps = { + children?: t.ReactNode; + theme?: t.CommonTheme; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.TooSmall/ui.tsx b/deploy/@tdb.slc/src/ui/ui.TooSmall/ui.tsx new file mode 100644 index 0000000000..b30a5e5d0b --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.TooSmall/ui.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { type t, Color, css, Icons, ReactString } from './common.ts'; + +type P = t.TooSmallProps; + +export const TooSmall: React.FC<P> = (props) => { + const {} = props; + + /** + * Render: + */ + const theme = Color.theme(props.theme); + const styles = { + base: css({ + position: 'relative', + color: theme.fg, + userSelect: 'none', + Padding: [20, 40], + fontSize: 16, + display: 'grid', + placeItems: 'center', + }), + body: css({ display: 'grid', gridTemplateRows: 'auto auto', rowGap: '1em', lineHeight: 1.5 }), + header: css({ display: 'grid', placeItems: 'center' }), + children: css({ textAlign: 'center' }), + }; + + const elHeader = ( + <div className={styles.header.class}> + <Icons.ProjectorScreen size={50} /> + </div> + ); + + const elBody = <div className={styles.children.class}>{wrangle.body(props.children)}</div>; + + return ( + <div className={css(styles.base, props.style).class}> + <div className={styles.body.class}> + {elHeader} + {elBody} + </div> + </div> + ); +}; + +/** + * Helpers: + */ +const wrangle = { + body(children?: t.ReactNode): t.ReactNode { + if (!children) return wrangle.body('Please make your window bigger.'); + if (typeof children === 'string') return ReactString.break(children); + return children; + }, +} as const; diff --git a/deploy/@tdb.slc/src/ui/ui.Video.Background/-SPEC.Debug.tsx b/deploy/@tdb.slc/src/ui/ui.Video.Background/-SPEC.Debug.tsx new file mode 100644 index 0000000000..8560b3a752 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Video.Background/-SPEC.Debug.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { App } from '../App/mod.ts'; +import { type t, Button, css, Signal, D } from './common.ts'; + +type P = t.VideoBackgroundProps; + +/** + * Types: + */ +export type DebugProps = { debug: DebugSignals; style?: t.CssInput }; +export type DebugSignals = ReturnType<typeof createDebugSignals>; + +/** + * Signals: + */ +export function createDebugSignals(init?: (e: DebugSignals) => void) { + const s = Signal.create; + const app = App.signals(); + const props = { + theme: s<t.CommonTheme>('Dark'), + }; + const api = { + props, + app, + listen() { + app.listen(); + const p = props; + p.theme.value; + }, + }; + + app.props.background.video.opacity.value = 0.6; + init?.(api); + return api; +} + +/** + * Component: + */ +export const Debug: React.FC<DebugProps> = (props) => { + const { debug } = props; + const app = debug.app; + const p = app.props; + const d = debug.props; + const bg = p.background; + + Signal.useRedrawEffect(() => debug.listen()); + + /** + * Render: + */ + const styles = { + base: css({}), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <Button + block + label={`theme: ${d.theme}`} + onClick={() => Signal.cycle<t.CommonTheme>(d.theme, ['Light', 'Dark'])} + /> + + <hr /> + <Button + block + label={`background.video.playing: ${bg.video.playing.value}`} + onClick={() => { + Signal.toggle(bg.video.playing); + }} + /> + <Button + block + label={`background.video.opacity: ${bg.video.opacity.value ?? '<undefined> (100%)'}`} + onClick={() => { + Signal.cycle<number | undefined>(bg.video.opacity, [0, 0.3, 0.6, undefined]); + }} + /> + + <hr /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Video.Background/-SPEC.tsx b/deploy/@tdb.slc/src/ui/ui.Video.Background/-SPEC.tsx new file mode 100644 index 0000000000..bdc86057ee --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Video.Background/-SPEC.tsx @@ -0,0 +1,29 @@ +import { Dev, Signal, Spec } from '../-test.ui.ts'; +import { Debug, createDebugSignals } from './-SPEC.Debug.tsx'; +import { VideoBackground } from './mod.ts'; + +export default Spec.describe('VideoBackground', (e) => { + const debug = createDebugSignals(); + const app = debug.app; + const p = app.props; + + e.it('init', (e) => { + const ctx = Spec.ctx(e); + + Dev.Theme.signalEffect(ctx, debug.props.theme, 1); + Signal.effect(() => { + debug.listen(); + ctx.redraw(); + }); + + ctx.subject + .size('fill') + .display('grid') + .render((e) => <VideoBackground state={app} />); + }); + + e.it('ui:debug', (e) => { + const ctx = Spec.ctx(e); + ctx.debug.row(<Debug debug={debug} />); + }); +}); diff --git a/deploy/@tdb.slc/src/ui/ui.Video.Background/common.ts b/deploy/@tdb.slc/src/ui/ui.Video.Background/common.ts new file mode 100644 index 0000000000..d75a28d192 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Video.Background/common.ts @@ -0,0 +1,9 @@ +export * from '../common.ts'; + +/** + * Constants: + */ +export const DEFAULTS = { + playing: true, +} as const; +export const D = DEFAULTS; diff --git a/deploy/@tdb.slc/src/ui/ui.Video.Background/mod.ts b/deploy/@tdb.slc/src/ui/ui.Video.Background/mod.ts new file mode 100644 index 0000000000..0b560fe60c --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Video.Background/mod.ts @@ -0,0 +1,4 @@ +/** + * @module + */ +export { VideoBackground } from './ui.tsx'; diff --git a/deploy/@tdb.slc/src/ui/ui.Video.Background/t.ts b/deploy/@tdb.slc/src/ui/ui.Video.Background/t.ts new file mode 100644 index 0000000000..f6f0b77ee3 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Video.Background/t.ts @@ -0,0 +1,9 @@ +import type { t } from './common.ts'; + +/** + * <Component>: + */ +export type VideoBackgroundProps = { + state: t.AppSignals; + style?: t.CssInput; +}; diff --git a/deploy/@tdb.slc/src/ui/ui.Video.Background/ui.tsx b/deploy/@tdb.slc/src/ui/ui.Video.Background/ui.tsx new file mode 100644 index 0000000000..bc74e21835 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/ui.Video.Background/ui.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { type t, css, D, VimeoBackground, Signal } from './common.ts'; + +type P = t.VideoBackgroundProps; + +export const VideoBackground: React.FC<P> = (props) => { + const { state } = props; + + const p = state.props.background.video; + const src = p.src.value; + const opacity = p.opacity.value; + const blur = p.blur.value; + const playing = p.playing.value ?? D.playing; + + const playerRef = React.useRef<t.VimeoIFrame>(); + + /** + * Effect: redraw (watch). + */ + Signal.useRedrawEffect(() => { + p.src.value; + p.opacity.value; + p.blur.value; + p.playing.value; + }); + + /** + * Render: + */ + const styles = { + base: css({ position: 'relative', pointerEvents: 'none' }), + video: css({ Absolute: 0 }), + }; + + return ( + <div className={css(styles.base, props.style).class}> + <VimeoBackground + video={src} + playing={playing} + blur={blur} + opacity={opacity} + onReady={(api) => (playerRef.current = api)} + style={styles.video} + /> + </div> + ); +}; diff --git a/deploy/@tdb.slc/src/ui/use/-.test.ts b/deploy/@tdb.slc/src/ui/use/-.test.ts new file mode 100644 index 0000000000..10fa9d00a8 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/use/-.test.ts @@ -0,0 +1,11 @@ +import { type t, describe, it, expect, c } from '../../-test.ts'; + +describe('Hooks', () => { + it('imports', async () => { + const m = await import('./mod.ts'); + console.info(c.brightGreen('(Hooks) Module:')); + console.info({ ...m }); + + expect(typeof m.useKeyboard == 'function').to.be.true; + }); +}); diff --git a/deploy/@tdb.slc/src/ui/use/common.ts b/deploy/@tdb.slc/src/ui/use/common.ts new file mode 100644 index 0000000000..8cae67176a --- /dev/null +++ b/deploy/@tdb.slc/src/ui/use/common.ts @@ -0,0 +1 @@ +export * from '../common.ts'; diff --git a/deploy/@tdb.slc/src/ui/use/mod.ts b/deploy/@tdb.slc/src/ui/use/mod.ts new file mode 100644 index 0000000000..780afa37e4 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/use/mod.ts @@ -0,0 +1 @@ +export * from './use.Keyboard.ts'; diff --git a/deploy/@tdb.slc/src/ui/use/t.ts b/deploy/@tdb.slc/src/ui/use/t.ts new file mode 100644 index 0000000000..6377ac4805 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/use/t.ts @@ -0,0 +1,6 @@ +import { type t } from './common.ts'; + +/** + * Hook: Keyboard controller. + */ +export type UseKeyboardFactory = (state?: t.AppSignals) => void; diff --git a/deploy/@tdb.slc/src/ui/use/use.Keyboard.ts b/deploy/@tdb.slc/src/ui/use/use.Keyboard.ts new file mode 100644 index 0000000000..a713f10bd4 --- /dev/null +++ b/deploy/@tdb.slc/src/ui/use/use.Keyboard.ts @@ -0,0 +1,26 @@ +import { useKeyboard as useDevKeyboard } from '@sys/ui-react-devharness'; +import { useEffect } from 'react'; +import { type t, Keyboard } from './common.ts'; + +/** + * Hook: Keyboard controller. + */ +export const useKeyboard: t.UseKeyboardFactory = (state) => { + useDevKeyboard(); + + useEffect(() => { + const keyboard = Keyboard.until(); + + /** + * š· START/STOP player + */ + keyboard.on('Space', () => { + /** + * TODO š· + */ + console.log('š· START/STOP player'); + }); + + return keyboard.dispose; + }, [state]); +}; diff --git a/deploy/@tdb.slc/vite.config.ts b/deploy/@tdb.slc/vite.config.ts new file mode 100644 index 0000000000..dd19226f00 --- /dev/null +++ b/deploy/@tdb.slc/vite.config.ts @@ -0,0 +1,16 @@ +import { Vite } from 'jsr:@sys/driver-vite'; +import { defineConfig } from 'npm:vite'; + +export default defineConfig(() => { + const entry = './src/-test/index.html'; + const paths = Vite.Config.paths({ app: { entry } }); + return Vite.Config.app({ + paths, + chunks(e) { + e.chunk('react', 'react'); + e.chunk('react.dom', 'react-dom'); + e.chunk('sys', ['@sys/std']); + e.chunk('css', ['@sys/ui-css']); + }, + }); +}); diff --git a/deploy/api.db.team/deno.json b/deploy/api.db.team/deno.json index aba5e127ba..d7d1d05d3c 100644 --- a/deploy/api.db.team/deno.json +++ b/deploy/api.db.team/deno.json @@ -1,6 +1,6 @@ { "name": "@tdb/api", - "version": "0.0.72", + "version": "0.0.82", "tasks": { "dev": "deno run -RNE --watch ./main.ts", "check": "deno check ./main.ts", diff --git a/deploy/slc.db.team/-design/NOTES.md b/deploy/slc.db.team/-design/NOTES.md deleted file mode 100644 index b354f6e3b8..0000000000 --- a/deploy/slc.db.team/-design/NOTES.md +++ /dev/null @@ -1,9 +0,0 @@ -```yaml notes -- Sample JSON/Timestamps for Video Player - - - -- Sample (historical) - https://socialleancanvas.com/ember-slc/ - -``` diff --git a/deploy/slc.db.team/-design/public/data/index.json b/deploy/slc.db.team/-design/public/data/index.json deleted file mode 100644 index aa89ae9fed..0000000000 --- a/deploy/slc.db.team/-design/public/data/index.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "kind": "dir", - "dir": { - "indexedAt": 1695690211367 - }, - "hash": { - "files": "sha256-5944ac2fa7a41caa297b32ef5b8bdec623f547b5ef1a976c65f536ef8ae16282", - "dir": "sha256-84280c1be94bef2b763966ec11d78df6232a390115fc6ca0b70ec7377906db77" - }, - "files": [ - { - "path": "index.json", - "bytes": 578, - "filehash": "sha256-38056e83a896e48bd2cd5cdd56d31cc14195f6781c476eabe96643f0bb6b0d19" - }, - { - "path": "log.json", - "bytes": 63, - "filehash": "sha256-7cbb17b6f2216f43d33eac4c3ee0979f8fabe48153ec93250201e65b6a9d6356" - }, - { - "path": "md/README.md", - "bytes": 57, - "filehash": "sha256-7a48826da2af737a86eada318b8bdf2e0e1db02e1592b44295068c25c4625d7f" - } - ] -} diff --git a/deploy/slc.db.team/-design/public/data/log.json b/deploy/slc.db.team/-design/public/data/log.json deleted file mode 100644 index 814122db7b..0000000000 --- a/deploy/slc.db.team/-design/public/data/log.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "latest": { - "version": "0.0.64" - }, - "history": [] -} diff --git a/deploy/slc.db.team/-design/public/data/md/README.md b/deploy/slc.db.team/-design/public/data/md/README.md deleted file mode 100644 index 6cb6fda7b5..0000000000 --- a/deploy/slc.db.team/-design/public/data/md/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Hello - -```yaml project.props -version: 0.0.0-draft.0 -``` \ No newline at end of file diff --git a/deploy/slc.db.team/-design/public/json/ember-pitch.json b/deploy/slc.db.team/-design/public/json/ember-pitch.json deleted file mode 100644 index 1110cf5847..0000000000 --- a/deploy/slc.db.team/-design/public/json/ember-pitch.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "slugs": [ - { - "kind": "slug:namespace", - "namespace": "ember.pitch", - "title": "Pitching" - }, - { - "id": "ember.pitch.intro", - "kind": "slug:VideoDiagram", - "split": 0.5, - "title": "Introduction", - "video": { - "innerScale": 1.1, - "src": { - "id": "847196819", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258939645-3d0a92fc-a173-4e2e-a7a9-b9608d8c0d27.png" - } - ] - } - }, - { - "id": "ember.pitch.structure", - "kind": "slug:VideoDiagram", - "split": 0.5, - "title": "Simple Structure", - "video": { - "innerScale": 1.1, - "src": { - "id": "846848747", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258939657-514453bd-a1fa-4463-bdc3-8ba853393400.png" - } - ] - } - }, - { - "id": "ember.pitch.example", - "kind": "slug:VideoDiagram", - "split": 0.5, - "title": "An Example", - "video": { - "innerScale": 1.1, - "src": { - "id": "846848687", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258939648-6cecd1ac-9e59-4512-8767-37cbe0e9ea86.png" - } - ] - } - }, - { - "id": "ember.pitch.succes", - "kind": "slug:VideoDiagram", - "split": 0.653, - "title": "Making it Memorable", - "video": { - "innerScale": 1.1, - "src": { - "id": "847199651", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258939654-212918dc-b58a-4b9e-b448-ed221b4142f7.png" - } - ] - } - }, - { - "id": "ember.pitch.conclusion", - "kind": "slug:VideoDiagram", - "split": 0.5, - "title": "Tie it all Together", - "video": { - "innerScale": 1.1, - "src": { - "id": "847193255", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258939662-4305ff3c-a432-4193-a1ba-fb1f3537af03.png" - } - ] - } - } - ] -} diff --git a/deploy/slc.db.team/-design/public/json/ember-slc.json b/deploy/slc.db.team/-design/public/json/ember-slc.json deleted file mode 100644 index 3a4a3d7430..0000000000 --- a/deploy/slc.db.team/-design/public/json/ember-slc.json +++ /dev/null @@ -1,2020 +0,0 @@ -{ - "slugs": [ - { - "kind": "slug:namespace", - "namespace": "intro", - "title": "Introduction" - }, - { - "id": "intro", - "kind": "slug:VideoDiagram", - "split": 0.653, - "title": "SLC Innovation Programme", - "video": { - "innerScale": 1.1, - "src": { - "id": "851209192", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258671853-81ef82e9-c2c0-4d59-a0e4-806c3a889cca.png", - "start": 0 - }, - { - "src": "https://user-images.githubusercontent.com/185555/258659458-ca448cab-383c-4170-80ba-d55e3c83a312.png", - "start": 64 - } - ] - } - }, - { - "id": "intro.programme", - "kind": "slug:VideoDiagram", - "split": 0.717, - "title": "About This Programme", - "video": { - "innerScale": 1.1, - "src": { - "id": "851211170", - "kind": "Vimeo" - }, - "timestamps": [ - { - "src": "https://user-images.githubusercontent.com/185555/258672044-788b5fda-85af-447c-8651-ce8b1030eda3.png", - "start": 0 - }, - { - "src": "https://user-images.githubusercontent.com/185555/258672166-576b41d7-dfa8-4b3f-9960-7bedbb188d54.png", - "start": 94 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258673262-d1314e2f-ed07-4ec1-a746-457cfe2ccff8.png", - "start": 116 - } - ] - } - }, - { - "id": "intro.canvas", - "kind": "slug:VideoDiagram", - "split": 0.653, - "title": "How to Use The Canvas", - "video": { - "innerScale": 1.05, - "src": { - "id": "851212258", - "kind": "Vimeo" - }, - "timestamps": [ - { - "src": "https://user-images.githubusercontent.com/185555/258673999-724e767c-560e-46fc-9868-ec9763cbb6b7.png", - "start": 0 - }, - { - "src": "https://user-images.githubusercontent.com/185555/258673947-c35bbfd9-e1a4-4974-bcf1-be970365b69d.png", - "start": 14 - }, - { - "src": "https://user-images.githubusercontent.com/185555/258673948-93447543-e794-42da-95e4-e47d5d1aa4ad.png", - "start": 21 - }, - { - "src": "https://user-images.githubusercontent.com/185555/258673999-724e767c-560e-46fc-9868-ec9763cbb6b7.png", - "start": 45 - }, - { - "src": "https://user-images.githubusercontent.com/185555/258672166-576b41d7-dfa8-4b3f-9960-7bedbb188d54.png", - "start": 122 - } - ] - } - }, - { - "id": "purpose", - "kind": "slug:VideoDiagram", - "split": 0.68, - "title": "Purpose", - "video": { - "innerScale": 1.01, - "src": { - "id": "577945255", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.6, - "src": "https://user-images.githubusercontent.com/185555/258676537-1f72bdfd-2c67-4286-aac8-b489204ba79e.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "customermodel", - "title": "Customer Model" - }, - { - "id": "customermodel.overview", - "kind": "slug:VideoDiagram", - "split": 0.64, - "title": "Customers", - "video": { - "innerScale": 1.02, - "src": { - "id": "577933592", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.6, - "src": "https://user-images.githubusercontent.com/185555/259265555-11f11af7-d993-4e81-bac6-d88fdcd315ad.png" - }, - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/259265623-95af98f0-e9b0-470f-b3df-d67075b79369.png" - } - ] - } - }, - { - "id": "customermodel.customers.1", - "kind": "slug:VideoDiagram", - "split": 0.693, - "title": "Customers (One)", - "video": { - "innerScale": 1.02, - "src": { - "id": "577928799", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258678843-72027c74-17f4-450c-806f-d4aeae158931.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.customers.2", - "kind": "slug:VideoDiagram", - "title": "Customers (Two)", - "video": { - "innerScale": 1.02, - "src": { - "id": "577928965", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258678843-72027c74-17f4-450c-806f-d4aeae158931.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.customers.earlyadopters", - "kind": "slug:VideoDiagram", - "title": "Early Adopters", - "video": { - "innerScale": 1.02, - "src": { - "id": "577929214", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258678843-72027c74-17f4-450c-806f-d4aeae158931.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.jobs", - "kind": "slug:VideoDiagram", - "title": "Jobs to be Done", - "video": { - "innerScale": 1.02, - "src": { - "id": "577933180", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258679138-f1f61d51-9803-4928-bbe5-790153e578eb.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.jobs.existingalternatives", - "kind": "slug:VideoDiagram", - "title": "Existing Alternatives ", - "video": { - "innerScale": 1.02, - "src": { - "id": "577933435", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258679138-f1f61d51-9803-4928-bbe5-790153e578eb.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.uvp", - "kind": "slug:VideoDiagram", - "title": "Unique Value Proposition", - "video": { - "innerScale": 1.02, - "src": { - "id": "577937937", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258679207-83301cb2-c07b-44a5-9178-0485f3679b8a.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.solution", - "kind": "slug:VideoDiagram", - "title": "Solution", - "video": { - "innerScale": 1.02, - "src": { - "id": "577937777", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258679288-5920e11b-331c-4158-87d4-b8bb8af5516b.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.example.FB.intro", - "kind": "slug:VideoDiagram", - "split": 0.677, - "title": "Example", - "video": { - "innerScale": 1.01, - "src": { - "id": "577929636", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258687717-9d34a221-1610-4a55-9157-a5a943fcfc9a.png", - "start": 0 - } - ] - } - }, - { - "id": "customermodel.example.FB.customers", - "kind": "slug:VideoDiagram", - "split": 0.683, - "title": "Example: Customers", - "video": { - "innerScale": 1.03, - "src": { - "id": "577929426", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258688907-9c1fd4ba-4727-41b0-9ce7-5407debed3c0.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258688921-347fbf6d-ad69-4157-855f-7457b38f8cc1.png", - "start": 8 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258688923-88d5ac08-5788-4734-8e46-78bf5b931f13.png", - "start": 35 - } - ] - } - }, - { - "id": "customermodel.example.FB.jobs", - "kind": "slug:VideoDiagram", - "split": 0.687, - "title": "Example: Jobs to be Done ", - "video": { - "innerScale": 1.03, - "src": { - "id": "577929737", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258690425-71eb6113-1541-48ae-b094-1f262fac6ac7.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258690432-e19aabbb-d9e1-4260-8cd0-0e483b52a2e4.png", - "start": 5 - } - ] - } - }, - { - "id": "customermodel.example.FB.UVP", - "kind": "slug:VideoDiagram", - "split": 0.71, - "title": "Example: UVP", - "video": { - "innerScale": 1.03, - "src": { - "id": "577933078", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258691202-19b6d32a-f225-4802-8b2c-2665c929fe46.png" - }, - { - "start": 5, - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258691209-4d84be14-65c9-4d7e-a49f-eb81657f2e45.png" - } - ] - } - }, - { - "id": "customermodel.example.FB.solution", - "kind": "slug:VideoDiagram", - "split": 0.713, - "title": "Example: Solution", - "video": { - "innerScale": 1.03, - "src": { - "id": "577932985", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258691728-1e92ad06-cfd1-4eb1-b9cb-6d740485a09a.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258691726-7e690b29-77d4-47f1-bc9f-65cef6f4860b.png", - "start": 17 - } - ] - } - }, - { - "id": "customermodel.example.FB.conclusion", - "kind": "slug:VideoDiagram", - "split": 0.707, - "title": "Example: Conclusion", - "video": { - "innerScale": 1.02, - "src": { - "id": "577929355", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258693837-92bb4005-ff1d-4654-85f3-b51feedebdeb.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "impactmodel", - "title": "Impact Model" - }, - { - "id": "impactmodel.overview", - "kind": "slug:VideoDiagram", - "split": 0.687, - "title": "Impact Model", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955565", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683136-72156ccf-9cf7-404c-9251-9e6ca40cb646.png", - "start": 0 - }, - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683177-16ad8903-e263-4f5b-b7d2-7604fcadbe70.png", - "start": 15 - } - ] - } - }, - { - "id": "impactmodel.overview.context", - "kind": "slug:VideoDiagram", - "title": "Context", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955697", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683323-e5282930-acbd-44f5-ae4c-b787da4a4202.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.overview.disclaimer", - "kind": "slug:VideoDiagram", - "title": "Just One Approach ", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955864", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683323-e5282930-acbd-44f5-ae4c-b787da4a4202.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.issue", - "kind": "slug:VideoDiagram", - "split": 0.723, - "title": "Issue", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955305", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683421-76d6631a-78a0-47b1-8743-f6d19382a2ce.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.participants", - "kind": "slug:VideoDiagram", - "split": 0.733, - "title": "Participants", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955976", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/259269324-ef25251c-b135-41f9-9fe9-520c9383ae30.png" - } - ] - } - }, - { - "id": "impactmodel.activities", - "kind": "slug:VideoDiagram", - "split": 0.69, - "title": "Activities", - "video": { - "innerScale": 1.02, - "src": { - "id": "577943744", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683501-35f87ff3-f808-4c3b-9ce2-46b24f061ce1.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.shorttermoutcomes", - "kind": "slug:VideoDiagram", - "split": 0.74, - "title": "Short Term Outcomes", - "video": { - "innerScale": 1.02, - "src": { - "id": "577956096", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683528-dc5670e3-d1c9-45dc-ad2d-845e7dcd51e9.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.mediumtermoutcomes", - "kind": "slug:VideoDiagram", - "split": 0.71, - "title": "Medium Term Outcomes", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955482", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683755-32d9afc4-6f62-483f-b0ef-41dd1f838d84.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.longtermoutcomes", - "kind": "slug:VideoDiagram", - "split": 0.733, - "title": "Long Term Outcomes", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955408", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683790-74900d97-7d28-4729-817b-686ef9932db3.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.impact", - "kind": "slug:VideoDiagram", - "title": "Impact ", - "video": { - "innerScale": 1.02, - "src": { - "id": "577955192", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258683884-a2649ae6-1205-425c-b082-b3e0d4d9335d.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.intro", - "kind": "slug:VideoDiagram", - "title": "Example", - "video": { - "innerScale": 1.02, - "src": { - "id": "577944077", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258694530-1ad84605-ef8a-4d46-b2a9-8e96b1f4c9b1.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.issue", - "kind": "slug:VideoDiagram", - "split": 0.733, - "title": "Example: Issue", - "video": { - "innerScale": 1.02, - "src": { - "id": "577944185", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258694807-54af1d7e-1198-40e4-a524-5a4d051351c0.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.participants", - "kind": "slug:VideoDiagram", - "split": 0.753, - "title": "Example: Participants", - "video": { - "innerScale": 1.02, - "src": { - "id": "577944806", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/259268593-2651e155-e1c2-484b-aa9b-6783971180f9.png" - } - ] - } - }, - { - "id": "impactmodel.example.activities", - "kind": "slug:VideoDiagram", - "split": 0.763, - "title": "Example: Activities", - "video": { - "innerScale": 1.02, - "src": { - "id": "577943848", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258695530-ed1df6d6-4b79-48bd-b0c7-ff37b4fab1bb.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.shorttermoutcomesI", - "kind": "slug:VideoDiagram", - "split": 0.75, - "title": "Example: Short-term Outcomes", - "video": { - "innerScale": 1.02, - "src": { - "id": "577944940", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258697747-108503dd-d81b-495e-b750-66bf68709120.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.mediumtermoutcomes", - "kind": "slug:VideoDiagram", - "split": 0.75, - "title": "Example: Medium-term Outcomes", - "video": { - "innerScale": 1.02, - "src": { - "id": "577944731", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258697741-0eacf674-633d-42e7-943c-713fcfb668fb.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.longtermoutcomes", - "kind": "slug:VideoDiagram", - "split": 0.75, - "title": "Example: Long-term Outcomes", - "video": { - "innerScale": 1.02, - "src": { - "id": "577944642", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258697731-77673802-b620-4869-bc95-9c18c769125d.png", - "start": 0 - } - ] - } - }, - { - "id": "impactmodel.example.impact", - "kind": "slug:VideoDiagram", - "split": 0.627, - "title": "Example: Impact", - "video": { - "innerScale": 1.02, - "src": { - "id": "577943950", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258697719-01e41c37-65f6-4db2-8b46-2882a0bf094c.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "metrics", - "title": "Economic Model" - }, - { - "id": "economicmodel.overview", - "kind": "slug:VideoDiagram", - "split": 0.543, - "title": "Economic Model", - "video": { - "innerScale": 1.02, - "src": { - "id": "577954558", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.6, - "src": "https://user-images.githubusercontent.com/185555/258698575-65aa349d-430a-4870-ae58-4a1635848511.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258698783-37d60998-478d-4709-8c73-a736e22e36b1.png", - "start": 18 - } - ] - } - }, - { - "id": "economicmodel.channels", - "kind": "slug:VideoDiagram", - "split": 0.703, - "title": "Channels", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938360", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258698984-df1da57d-f508-41d8-964a-c8537e0c6a91.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.financialmodel.intro", - "kind": "slug:VideoDiagram", - "split": 0.69, - "title": "Financial Model (Intro)", - "video": { - "innerScale": 1.02, - "src": { - "id": "577954346", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258699030-ead93fa0-83a9-4586-bb18-9b51c13690c5.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.financialmodel.revenue", - "kind": "slug:VideoDiagram", - "split": 0.69, - "title": "Revenue", - "video": { - "innerScale": 1.02, - "src": { - "id": "577954492", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258699083-0fc5bd70-2cbf-4a56-98cc-f8fd0c63d38e.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.financialmodel.costs", - "kind": "slug:VideoDiagram", - "split": 0.687, - "title": "Costs", - "video": { - "innerScale": 1.02, - "src": { - "id": "577954228", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258699124-0fe80d38-db0d-43d3-ab6a-507108e57869.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.advantage.1leverage", - "kind": "slug:VideoDiagram", - "split": 0.683, - "title": "Advantage: Leverage", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938084", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258699180-af0c5f15-b019-4a45-8a02-2960899387db.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.advantage.2innovate", - "kind": "slug:VideoDiagram", - "split": 0.687, - "title": "Advantage: Innovate", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938331", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258699180-af0c5f15-b019-4a45-8a02-2960899387db.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.FB.example.overview", - "kind": "slug:VideoDiagram", - "split": 0.48, - "title": "Example", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938899", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258701584-bc064632-7492-4f1e-b333-4d111af63126.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.FB.example.channels", - "kind": "slug:VideoDiagram", - "split": 0.673, - "title": "Example: Channels", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938645", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258701634-3e8b3437-2964-4427-b750-11d1ee4110e3.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258701742-ad0e2ad1-c5cf-4921-a830-bb97810fbe72.png", - "start": 0 - } - ] - } - }, - { - "id": "economicmodel.FB.example.financialmodel", - "kind": "slug:VideoDiagram", - "split": 0.667, - "title": "Example: Financial Model", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938753", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258702272-b45f3292-fee0-4305-8574-8cd838e7d86a.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258702294-b3c8b9c2-50b0-4538-9d1e-7301dfe44e4f.png", - "start": 6 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258702298-2f342a99-2b5b-4b88-8544-aba3234000c6.png", - "start": 25 - } - ] - } - }, - { - "id": "economicmodel.FB.example.advantage", - "kind": "slug:VideoDiagram", - "split": 0.667, - "title": "Example: Advantage", - "video": { - "innerScale": 1.02, - "src": { - "id": "577938493", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258702762-cc2c78dc-8f66-4dd7-9f25-ba503f040a6c.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258702765-53af8ba0-9955-4299-a5d6-1b73edaeafce.png", - "start": 14 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "metrics", - "title": "Metrics" - }, - { - "id": "metrics.keymetrics", - "kind": "slug:VideoDiagram", - "title": "Key Metrics", - "video": { - "innerScale": 1.02, - "src": { - "id": "577945041", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.6, - "src": "https://user-images.githubusercontent.com/185555/258703729-9d2e7906-3079-4299-8ffe-546caf77f941.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "conclusion", - "title": "Conclusion" - }, - { - "id": "canvas.conclusion", - "kind": "slug:VideoDiagram", - "title": "When you've finished", - "video": { - "innerScale": 1.02, - "src": { - "id": "851213132", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258672044-788b5fda-85af-447c-8651-ce8b1030eda3.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "strategy", - "title": "Strategy" - }, - { - "id": "strategy.intro", - "kind": "slug:VideoDiagram", - "title": "Introduction to Strategy", - "video": { - "innerScale": 1.02, - "src": { - "id": "851213828", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258672166-576b41d7-dfa8-4b3f-9960-7bedbb188d54.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.intro.2", - "kind": "slug:VideoDiagram", - "split": 0.693, - "title": "Getting Started", - "video": { - "innerScale": 1.02, - "src": { - "id": "598791412", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258672166-576b41d7-dfa8-4b3f-9960-7bedbb188d54.png", - "start": 0 - }, - { - "scale": 0.85, - "src": "https://user-images.githubusercontent.com/185555/258704864-6cf19bfd-6bfe-4148-aac8-e9bc6f327962.png", - "start": 54 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "startupjourney", - "title": "Startup Journey" - }, - { - "id": "startupjourney.intro", - "kind": "slug:VideoDiagram", - "split": 0.41, - "title": "The Startup Journey", - "video": { - "innerScale": 1.02, - "src": { - "id": "598792779", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258706852-98db5f16-c2ba-4e4b-bb3f-eef4a151121d.png", - "start": 0 - } - ] - } - }, - { - "id": "startupjourney.2", - "kind": "slug:VideoDiagram", - "split": 0.657, - "title": "Definition of a Startup", - "video": { - "innerScale": 1.02, - "src": { - "id": "598791681", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258707039-92bfe94b-a63e-4bf4-8eb1-df87be2eb4c9.png", - "start": 0 - }, - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258707140-b2df4a87-8bae-4589-a2cd-5a89cf9155bd.png", - "start": 12 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258707039-92bfe94b-a63e-4bf4-8eb1-df87be2eb4c9.png", - "start": 36 - } - ] - } - }, - { - "id": "startupjourney.3", - "kind": "slug:VideoDiagram", - "split": 0.643, - "title": "From Startup to Business", - "video": { - "innerScale": 1.02, - "src": { - "id": "598791979", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258707436-fd39d04a-effa-4d88-a43d-16560f676abb.png", - "start": 0 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258707464-0fc4d6c6-0bc1-4d15-9fe0-18e46a3644e3.png", - "start": 6 - }, - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258707493-310febee-fc1f-48e1-bfec-51fa05585dfc.png", - "start": 40 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258707524-3b05f66f-8ac4-44be-a9f6-862862b05611.png", - "start": 56 - } - ] - } - }, - { - "id": "startupjourney.4", - "kind": "slug:VideoDiagram", - "split": 0.74, - "title": "From Founders to Employees", - "video": { - "innerScale": 1.02, - "src": { - "id": "598792314", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258708016-bf798904-635a-44a9-a6d0-2ac9015e4f96.png", - "start": 0 - } - ] - } - }, - { - "id": "startupjourney.5", - "kind": "slug:VideoDiagram", - "split": 0.65, - "title": "The Three Circles", - "video": { - "innerScale": 1.02, - "src": { - "id": "598792522", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258711740-d4b70a85-5486-4512-b89c-75debe92ca3d.png", - "start": 0 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258711782-62e3a768-68d5-406c-85e2-78f9e95eeb39.png", - "start": 41 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258711802-58242189-bd60-4789-9e71-94d6cad515af.png", - "start": 51 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258711759-f9171352-30b6-40f6-87bd-f43963aa85e5.png", - "start": 75 - } - ] - } - }, - { - "id": "startupjourney5.eg", - "kind": "slug:VideoDiagram", - "split": 0.643, - "title": "Example", - "video": { - "innerScale": 1.02, - "src": { - "id": "598793055", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258711740-d4b70a85-5486-4512-b89c-75debe92ca3d.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "goodbm", - "title": "Good Business Model" - }, - { - "id": "goodbm.intro", - "kind": "slug:VideoDiagram", - "split": 0.767, - "title": "A Good Business Model", - "video": { - "innerScale": 1.02, - "src": { - "id": "598789079", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258715942-01c9eb71-1325-4211-9b06-d12ba7e63fe7.png", - "start": 0 - }, - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258715968-6d478f21-f15d-4f5e-8a1d-dffdd11400ae.png", - "start": 68 - } - ] - } - }, - { - "id": "goodbm.2.customermodel", - "kind": "slug:VideoDiagram", - "title": "Product Market Fit", - "video": { - "innerScale": 1.02, - "src": { - "id": "598757457", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258716362-22cb4a33-5321-4c6d-95ec-26b90a81eb70.png", - "start": 0 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258716385-d5ff0a7b-cbb9-45bf-9401-efc247e71ff6.png", - "start": 8 - } - ] - } - }, - { - "id": "goodbm.3.impactmodel", - "kind": "slug:VideoDiagram", - "title": "Impact Micro / Macro", - "video": { - "innerScale": 1.02, - "src": { - "id": "598757720", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258716752-50ef5a31-5c5b-4973-9d20-7026ef348e87.png", - "start": 0 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258716776-213794cc-6087-4052-a9a0-96b9703ba645.png", - "start": 8 - } - ] - } - }, - { - "id": "goodbm.4.economicmodel.intro", - "kind": "slug:VideoDiagram", - "split": 0.683, - "title": "Economic Model", - "video": { - "innerScale": 1.02, - "src": { - "id": "598758279", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.7, - "src": "https://user-images.githubusercontent.com/185555/258717247-558d0b0f-3825-4e8f-b910-f9c2acd0d86b.png", - "start": 0 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258717383-dc18622b-77d4-4cc2-bfc2-dccf339d817f.png", - "start": 18 - } - ] - } - }, - { - "id": "goodbm.4.economicmodel.uniteconomics.1", - "kind": "slug:VideoDiagram", - "split": 0.753, - "title": "Positive Unit Economics ", - "video": { - "innerScale": 1.02, - "src": { - "id": "598758327", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718196-3ab075d8-59cb-46f7-9da3-7ae26aa8251c.png", - "start": 0 - }, - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718248-578d9842-4c16-46b1-88fa-70e474373ced.png", - "start": 15 - } - ] - } - }, - { - "id": "goodbm.4.economicmodel.uniteconomics.2", - "kind": "slug:VideoDiagram", - "split": 0.783, - "title": "Calculating", - "video": { - "innerScale": 1.02, - "src": { - "id": "598758555", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718409-aba235d8-2008-4dff-ad3a-bba247be8702.png", - "start": 0 - }, - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718479-d9b84723-b9dd-4351-b67a-042dd4d86e05.png", - "start": 62 - } - ] - } - }, - { - "id": "goodbm.4.economicmodel.growth", - "kind": "slug:VideoDiagram", - "title": "Can You Grow?", - "video": { - "innerScale": 1.02, - "src": { - "id": "598757995", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718724-3b12ff14-e4f1-4f25-a190-76bc53e686bf.png", - "start": 0 - }, - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718738-170d388d-de31-4afe-bd70-f613352e6af8.png", - "start": 4 - }, - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258718767-410b3c52-416a-41bb-9004-32297fad4296.png", - "start": 25 - } - ] - } - }, - { - "id": "goodbm.conclusion", - "kind": "slug:VideoDiagram", - "split": 0.387, - "title": "Conclusion", - "video": { - "innerScale": 1.02, - "src": { - "id": "598758962", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258722197-38fbb2cb-c3d5-4f75-80c9-aeef0200a6ba.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "goodbm.improve", - "title": "Improvement" - }, - { - "id": "goodbm.improve.jeff", - "kind": "slug:VideoDiagram", - "title": "Improve Your Business Model", - "video": { - "innerScale": 1.02, - "src": { - "id": "598759314", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258722963-f8da0a30-2477-4d7f-aadf-ec2097d63b3f.png", - "start": 0 - }, - { - "scale": 0.74, - "src": "https://user-images.githubusercontent.com/185555/258723084-170d2d17-5d56-494e-9ac4-68d5176de157.png", - "start": 12 - }, - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258723112-642cb060-c2db-413f-97ed-67ca0eb273e1.png", - "start": 55 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.intro", - "kind": "slug:VideoDiagram", - "split": 0.727, - "title": "Leverage", - "video": { - "innerScale": 1.02, - "src": { - "id": "598788777", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258723704-9852d071-5eb1-44d4-b3ec-7ff3e386fb1a.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.intro", - "kind": "slug:VideoDiagram", - "split": 0.667, - "title": "Types of Leverage", - "video": { - "innerScale": 1.02, - "src": { - "id": "598786562", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724142-506416bd-3e64-41d9-b85e-8315d2254daa.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.labour", - "kind": "slug:VideoDiagram", - "title": "Labour", - "video": { - "innerScale": 1.02, - "src": { - "id": "598786787", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724235-ad03d995-eee1-486a-8f13-b8658cd81c5b.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.technology", - "kind": "slug:VideoDiagram", - "title": "Technology", - "video": { - "innerScale": 1.15, - "src": { - "id": "598787988", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724317-a78a3bc6-fcd5-4cb0-b1d0-113f0d94ae74.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.capital", - "kind": "slug:VideoDiagram", - "title": "Capital", - "video": { - "innerScale": 1.02, - "src": { - "id": "598759864", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724389-d57fc9e2-bb59-4d2d-a646-3703493f79aa.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.Product", - "kind": "slug:VideoDiagram", - "title": "Product", - "video": { - "innerScale": 1.02, - "src": { - "id": "598787543", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724467-273fabf7-4e97-4a4a-bae0-882ec8b5da47.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.media", - "kind": "slug:VideoDiagram", - "title": "Media", - "video": { - "innerScale": 1.02, - "src": { - "id": "598786893", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724550-96195499-9f2c-4cc5-a618-1870d525defa.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.perceptual", - "kind": "slug:VideoDiagram", - "title": "Perceptual", - "video": { - "innerScale": 1.02, - "src": { - "id": "598787297", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724616-d1a76df8-a4e0-4482-9d9d-85f6cade59fe.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.network", - "kind": "slug:VideoDiagram", - "title": "Network", - "video": { - "innerScale": 1.02, - "src": { - "id": "598787119", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724693-57184816-71bb-43df-b0dd-494327d72e57.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.2types.strategic", - "kind": "slug:VideoDiagram", - "split": 0.753, - "title": "Strategic", - "video": { - "innerScale": 1.02, - "src": { - "id": "598787671", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724758-314e94b2-b9bc-4da9-9ac3-44763fd67ceb.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.3size.1", - "kind": "slug:VideoDiagram", - "split": 0.663, - "title": "Enterprise Size", - "video": { - "innerScale": 1.02, - "src": { - "id": "598788082", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258724873-6813f52a-01de-48bc-bcfe-49d7fcb79e81.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.leverage.3size.2", - "kind": "slug:VideoDiagram", - "title": "Enterprise Complexity", - "video": { - "innerScale": 1.02, - "src": { - "id": "598788467", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258724944-7a99c045-65e0-4e0b-81a0-d2363b6cca39.png", - "start": 0 - } - ] - } - }, - { - "id": "goodbm.improve.conclusion", - "kind": "slug:VideoDiagram", - "split": 0.773, - "title": "Conclusion ", - "video": { - "innerScale": 1.02, - "src": { - "id": "598759076", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258725150-f1a44a40-21fc-406a-94aa-7c330dd24ede.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "strategy.stage", - "title": "Strategy Stage" - }, - { - "id": "strategy.stage.intro", - "kind": "slug:VideoDiagram", - "title": "Stage", - "video": { - "innerScale": 1.02, - "src": { - "id": "598793514", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258729336-52097361-322e-4bb2-a0e6-9f2b39aee9c9.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.stage.1", - "kind": "slug:VideoDiagram", - "split": 0.72, - "title": "Assessing Your Stage", - "video": { - "innerScale": 1.02, - "src": { - "id": "598795704", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.97, - "src": "https://user-images.githubusercontent.com/185555/258729479-036ae33e-47d0-4533-b08d-b0388484d5da.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.stage.2", - "kind": "slug:VideoDiagram", - "split": 0.72, - "title": "The Venture Map", - "video": { - "innerScale": 1.02, - "src": { - "id": "598795918", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258729516-fa75631c-32ef-45f1-91dc-9370a67aaddf.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.stage.3", - "kind": "slug:VideoDiagram", - "split": 0.717, - "title": "Conclusions", - "video": { - "innerScale": 1.02, - "src": { - "id": "598796095", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.85, - "src": "https://user-images.githubusercontent.com/185555/258729578-3c37e55e-4aa1-45c5-9c74-78d01dc42962.png", - "start": 0 - } - ] - } - }, - { - "kind": "slug:namespace", - "namespace": "strategy.models", - "title": "Strategy Models" - }, - { - "id": "strategy.models.intro", - "kind": "slug:VideoDiagram", - "split": 0.62, - "title": "Strategy", - "video": { - "innerScale": 1.02, - "src": { - "id": "598794082", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.8, - "src": "https://user-images.githubusercontent.com/185555/258731304-0c36ff18-5adb-4ca7-84c6-679f024a6fc2.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.models.2", - "kind": "slug:VideoDiagram", - "split": 0.743, - "title": "Canvas Models", - "video": { - "innerScale": 1.02, - "src": { - "id": "598793732", - "kind": "Vimeo" - }, - "timestamps": [ - { - "start": 0, - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258731717-f2698344-9b96-46d7-8237-069d1cb9b3a8.png" - }, - { - "start": 60, - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/259271445-e0a699f2-5782-4c40-ba5d-f77f803b7090.png" - }, - { - "start": 67, - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/259271453-bd6b4125-498d-4a56-848e-1db0737a4fa1.png" - }, - { - "start": 76, - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/259271461-ebd7d480-178e-4e5c-a9b1-6f98e19fd192.png" - } - ] - } - }, - { - "id": "strategy.models.validation", - "kind": "slug:VideoDiagram", - "split": 0.727, - "title": "Validation", - "video": { - "innerScale": 1.02, - "src": { - "id": "598795327", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258732088-883064dc-60be-44f3-b59e-430e66bb05f5.png", - "start": 0 - }, - { - "scale": 0.9, - "src": "https://user-images.githubusercontent.com/185555/258732112-cdd6881b-539c-4ce2-8ad5-0026b5bede20.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.models.together", - "kind": "slug:VideoDiagram", - "split": 0.72, - "title": "Pulling Your Strategy Together", - "video": { - "innerScale": 1.02, - "src": { - "id": "598796367", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.95, - "src": "https://user-images.githubusercontent.com/185555/258732446-070a44a1-3c7b-4415-a03d-2e641f526f9d.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.timeline", - "kind": "slug:VideoDiagram", - "split": 0.723, - "title": "Strategy Timeline", - "video": { - "innerScale": 1.02, - "src": { - "id": "851214652", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 0.97, - "src": "https://user-images.githubusercontent.com/185555/258733175-6c16ec25-0395-4629-a267-f7bb537656c0.png", - "start": 0 - } - ] - } - }, - { - "id": "strategy.conclusion", - "kind": "slug:VideoDiagram", - "split": 0.717, - "title": "Conclusion", - "video": { - "innerScale": 1.02, - "src": { - "id": "598793191", - "kind": "Vimeo" - }, - "timestamps": [ - { - "scale": 1, - "src": "https://user-images.githubusercontent.com/185555/258735372-664939ab-6574-4aca-8d05-0901cab728fc.png", - "start": 0 - } - ] - } - } - ] -} diff --git a/deploy/slc.db.team/deno.json b/deploy/slc.db.team/deno.json deleted file mode 100644 index 468a77373c..0000000000 --- a/deploy/slc.db.team/deno.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@tdb/slc", - "version": "0.0.68", - "license": "MIT", - "tasks": { - "start": "deno run -RNE --watch ./src/s.main/http-server.ts", - "lint": "deno lint", - "dry": "deno publish --allow-dirty --dry-run", - "clean": "deno run -RWE ./scripts/-clean.ts", - "test": "deno test -RWNE --allow-run --allow-ffi", - "prep": "deno run -RWE ./scripts/-prep.ts", - "dev": "deno run -RWNE --allow-run --allow-ffi ./scripts/-dev.ts", - "build": "deno run -RWNE --allow-run --allow-ffi ./scripts/-build.ts", - "serve": "deno run -RNE --allow-run jsr:@sys/http/server/start", - "deploy": "deno task dry && deployctl deploy --prod --org=tdb --project=tdb-slc" - }, - "exports": { - ".": "./src/mod.ts", - "./t": "./src/types.ts", - "./types": "./src/types.ts" - }, - "deploy": { - "exclude": [ - "**/node_modules" - ], - "include": [], - "entrypoint": "src/s.main/http-server.ts" - } -} diff --git a/deploy/slc.db.team/scripts/-build.ts b/deploy/slc.db.team/scripts/-build.ts deleted file mode 100644 index ae681c7ddf..0000000000 --- a/deploy/slc.db.team/scripts/-build.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Fs } from '@sys/fs'; -import { Vite } from '@sys/driver-vite'; -import { pkg } from '../src/pkg.ts'; - -const input = './src/-test/index.html'; -const bundle = await Vite.build({ pkg, input }); -console.info(bundle.toString({ pad: true })); - -/** - * Ensure the {manifest.json} file exists. - */ -const from = Fs.resolve('./src/manifest.json'); -const to = Fs.resolve('./dist/manifest.json'); -await Fs.copy(from, to, { throw: true }); diff --git a/deploy/slc.db.team/scripts/-clean.ts b/deploy/slc.db.team/scripts/-clean.ts deleted file mode 100644 index 30ae5e6470..0000000000 --- a/deploy/slc.db.team/scripts/-clean.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Fs } from '@sys/fs'; - -const removeDir = (path: string) => Fs.remove(Fs.resolve(path), { log: true }); -await removeDir('./.tmp'); diff --git a/deploy/slc.db.team/scripts/-dev.ts b/deploy/slc.db.team/scripts/-dev.ts deleted file mode 100644 index 530ecf6199..0000000000 --- a/deploy/slc.db.team/scripts/-dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Vite } from '@sys/driver-vite'; -import { pkg } from '../src/pkg.ts'; - -const input = './src/-test/index.html'; -const server = await Vite.dev({ pkg, input }); -await server.listen(); diff --git a/deploy/slc.db.team/scripts/-prep.ts b/deploy/slc.db.team/scripts/-prep.ts deleted file mode 100644 index 50e80eb1ba..0000000000 --- a/deploy/slc.db.team/scripts/-prep.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Fs } from '@sys/fs'; - -const PATH = { - from: '/Users/phil/code/samples/vitepress-slc/docs/.vitepress/dist', - to: Fs.resolve('./dist.docs'), -}; - -const res = await Fs.copy(PATH.from, PATH.to, { force: true }); -console.log('res', res); diff --git a/deploy/slc.db.team/src/-mod.test.ts b/deploy/slc.db.team/src/-mod.test.ts deleted file mode 100644 index 174dbeeac3..0000000000 --- a/deploy/slc.db.team/src/-mod.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it, pkg } from './-test.ts'; - -describe(`Pkg: ${pkg.name}@${pkg.version}`, () => { - it('š· placeholder', () => { - expect(123).to.equal(123); - }); -}); diff --git a/deploy/slc.db.team/src/-test.ts b/deploy/slc.db.team/src/-test.ts deleted file mode 100644 index ad27e650df..0000000000 --- a/deploy/slc.db.team/src/-test.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './-test/mod.ts'; diff --git a/deploy/slc.db.team/src/-test/entry.tsx b/deploy/slc.db.team/src/-test/entry.tsx deleted file mode 100644 index b253d5cf25..0000000000 --- a/deploy/slc.db.team/src/-test/entry.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; - -import { pkg } from '../common.ts'; -import { Tmp } from '../mod.ts'; - -/** - * Render UI. - */ -const document = globalThis.document; -document.title = pkg.name; -console.log('š· entry.tsx', pkg); - -const root = createRoot(document.getElementById('root')); -root.render( - <StrictMode> - <Tmp /> - </StrictMode>, -); diff --git a/deploy/slc.db.team/src/-test/mod.ts b/deploy/slc.db.team/src/-test/mod.ts deleted file mode 100644 index 7539a23ea4..0000000000 --- a/deploy/slc.db.team/src/-test/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Testing, describe, expect, it } from '@sys/testing/server'; -export * from '../common.ts'; diff --git a/deploy/slc.db.team/src/common/libs.ts b/deploy/slc.db.team/src/common/libs.ts deleted file mode 100644 index 25445ccbbf..0000000000 --- a/deploy/slc.db.team/src/common/libs.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { Err, Hash, Path, Pkg, Time, rx } from '@sys/std'; - -export { COLORS, Str } from '@sys/std'; -export { Color, css } from '@sys/ui-css/react'; -export { FC } from '@sys/ui-react'; diff --git a/deploy/slc.db.team/src/common/t.ts b/deploy/slc.db.team/src/common/t.ts deleted file mode 100644 index e83af329a1..0000000000 --- a/deploy/slc.db.team/src/common/t.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type { - CommonTheme, - Disposable, - DistPkg, - Lifecycle, - Msecs, - StdError, - StringHash, - UntilObservable, -} from '@sys/types'; - -export type { CssValue } from '@sys/ui-css/t'; - -export type * from '../types.ts'; diff --git a/deploy/slc.db.team/src/manifest.json b/deploy/slc.db.team/src/manifest.json deleted file mode 100644 index 093787acf1..0000000000 --- a/deploy/slc.db.team/src/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@tdb/slc", - "short_name": "@tdb/slc", - "start_url": "/", - "display": "standalone", - "background_color": "#FE0064", - "theme_color": "#FE0064" -} diff --git a/deploy/slc.db.team/src/mod.ts b/deploy/slc.db.team/src/mod.ts deleted file mode 100644 index e147b64cee..0000000000 --- a/deploy/slc.db.team/src/mod.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @module - * Tools for ... - * - * @example - * ```ts - * // sample - * ``` - */ -export { pkg } from './pkg.ts'; -export type * as t from './types.ts'; - -export { Tmp } from './ui/Tmp.tsx'; diff --git a/deploy/slc.db.team/src/pkg.ts b/deploy/slc.db.team/src/pkg.ts deleted file mode 100644 index 79cb3f9c80..0000000000 --- a/deploy/slc.db.team/src/pkg.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Pkg, type t } from '@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - - -/** - * Package meta-data. - */ -export const pkg: t.Pkg = Pkg.fromJson(deno); diff --git a/deploy/slc.db.team/src/s.main/http-server.ts b/deploy/slc.db.team/src/s.main/http-server.ts deleted file mode 100644 index d717427604..0000000000 --- a/deploy/slc.db.team/src/s.main/http-server.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Pkg } from 'jsr:@sys/fs'; -import { HttpServer } from 'jsr:@sys/http/server'; -import { pkg } from './pkg.ts'; - -const dist = (await Pkg.Dist.load('./dist')).dist; -const hash = dist?.hash.digest ?? ''; - -/** - * Define HTTP Web Server. - */ -const app = HttpServer.create({ pkg, hash }); - -/** - * Docs - */ -app.get('/docs', HttpServer.static({ root: './dist.docs', path: '/index.html' })); -app.use( - '/docs/*', - HttpServer.static({ - root: './dist.docs', - rewriteRequestPath: (path) => path.replace(/^\/docs/, ''), - }), -); - -// Serve static files from /dist for all other paths. -app.use('/*', HttpServer.static({ root: './dist' })); - -/** - * Start Server. - */ -const config = HttpServer.options(8080, pkg, hash); -Deno.serve(config, app.fetch); - -/** - * Sample static server middleware. - */ - -// const staticMiddleware = (rootDir: string) => { -// return async (c: Context, next: Next) => { -// const url = new URL(c.req.url); -// let filepath = decodeURIComponent(url.pathname); -// -// // Prevent directory traversal attacks -// if (filepath.includes('..')) { -// return c.text('Forbidden', 403); -// } -// -// // Map URL path to filesystem path -// filepath = `${rootDir}${filepath}`; -// -// try { -// let fileInfo = await Deno.stat(filepath); -// -// // If it's a directory, try to serve index.html -// if (fileInfo.isDirectory) { -// filepath = `${filepath}/index.html`; -// fileInfo = await Deno.stat(filepath); // Re-stat the index.html file -// } -// -// // Read the file content -// const file = await Deno.readFile(filepath); -// -// // Determine the content type -// const contentType = lookup(filepath) || 'application/octet-stream'; -// -// // Return the file content -// return c.body(file, 200, { -// 'Content-Type': contentType, -// }); -// } catch (e) { -// if (e instanceof Deno.errors.NotFound) { -// // File not found, proceed to the next middleware or route -// await next(); -// } else { -// // Other errors (e.g., permission issues) -// return c.text('Internal Server Error', 500); -// } -// } -// }; -// }; diff --git a/deploy/slc.db.team/src/s.main/pkg.ts b/deploy/slc.db.team/src/s.main/pkg.ts deleted file mode 100644 index 9e44b0ad96..0000000000 --- a/deploy/slc.db.team/src/s.main/pkg.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Pkg, type t } from 'jsr:@sys/std'; -import { default as deno } from '../../deno.json' with { type: 'json' }; - -/** - * Package meta-data. - */ -export const pkg: t.Pkg = Pkg.fromJson(deno); diff --git a/deploy/slc.db.team/src/types.ts b/deploy/slc.db.team/src/types.ts deleted file mode 100644 index 0f2f882807..0000000000 --- a/deploy/slc.db.team/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @module - * Module types. - */ -export {}; diff --git a/deploy/slc.db.team/src/ui/Tmp.tsx b/deploy/slc.db.team/src/ui/Tmp.tsx deleted file mode 100644 index 86fdf97611..0000000000 --- a/deploy/slc.db.team/src/ui/Tmp.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useEffect, useState } from 'react'; -import { type t, Color, COLORS, css, Hash, Pkg, pkg, rx, Str } from './common.ts'; - -export type TmpProps = { - digest?: t.StringHash; - theme?: t.CommonTheme; - style?: t.CssInput; -}; - -export const Tmp: React.FC<TmpProps> = (props) => { - const [_dist, setDist] = useState<t.DistPkg>(); - const dist: t.DistPkg | undefined = _dist; - const digest = props.digest ? wrangle.fmtHash(props.digest) : wrangle.digest(dist); - - /** - * Lifecycle. - */ - useEffect(() => { - const { dispose, dispose$ } = rx.disposable(); - (async () => { - /** - * GET fetch - */ - const res = await Pkg.Dist.fetch({ dispose$, disposeReason: 'react:useEffect:dispose' }); - setDist(res.dist); - })(); - return dispose; - }, []); - - /** - * Render - */ - const theme = Color.theme(props.theme ?? 'Dark'); - const styles = { - base: css({ - Absolute: 0, - backgroundColor: COLORS.MAGENTA, - color: theme.fg, - fontFamily: 'sans-serif', - }), - body: { - base: css({ Absolute: 0, display: 'grid', placeItems: 'center', userSelect: 'none' }), - inner: css({ marginBottom: 25 }), - pig: css({ fontSize: 50 }), - title: css({ fontSize: 28 }), - }, - pkg: { - base: css({ - Absolute: [null, null, 16, digest.display ? 18 : 15], - fontFamily: 'monospace', - cursor: 'pointer', - display: 'grid', - rowGap: '0.5em', - }), - at: css({ MarginX: '0.6em', opacity: 0.5 }), - version: css({ opacity: 1 }), - hash: css({ opacity: 0.5 }), - name: css({}), - }, - a: css({ - textDecoration: 'none', - color: theme.fg, - }), - }; - - const elBody = ( - <div {...styles.body.base}> - <div {...styles.body.inner}> - <a {...styles.a} href="./docs"> - <div {...styles.body.pig}>{`š·`}</div> - <div {...styles.body.title}>{`Social Lean Canvas`}</div> - </a> - </div> - </div> - ); - - const elHash = digest.display && ( - <div {...styles.pkg.hash} title={digest.tooltip}> - {digest.display} - </div> - ); - - const elNameVersion = ( - <div> - <div> - <span {...styles.pkg.name}>{pkg.name}</span> - <span {...styles.pkg.at}>{'@'}</span> - <span {...styles.pkg.version}>{pkg.version}</span> - </div> - </div> - ); - - const elPkg = ( - <div {...styles.pkg.base}> - {elNameVersion} - {elHash} - </div> - ); - - return ( - <div style={css(styles.base, props.style)}> - {elBody} - {elPkg} - </div> - ); -}; - -/** - * Helpers - */ -const wrangle = { - digest(dist?: t.DistPkg) { - const digest = wrangle.fmtHash(dist?.hash.digest); - const b = dist?.size.bytes; - const bytes = Str.bytes(b); - const display = `${digest.display} (${bytes})`; - return { ...digest, display }; - }, - - fmtHash(hash?: t.StringHash) { - const long = hash ?? ''; - const short = Hash.shorten(long, [0, 4], true); - const tooltip = `pkg:digest:${long}`; - const display = `pkg:sha256:#${short}`; - return { long, short, display, tooltip }; - }, -} as const; diff --git a/deploy/slc.db.team/vite.config.ts b/deploy/slc.db.team/vite.config.ts deleted file mode 100644 index 25af1be683..0000000000 --- a/deploy/slc.db.team/vite.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Vite } from '@sys/driver-vite'; -import { defineConfig } from 'vite'; -import { pkg } from './src/pkg.ts'; - -export default defineConfig(() => { - return Vite.Plugin.common({ - pkg, - chunks(e) { - e.chunk('react', 'react'); - e.chunk('react.dom', 'react-dom'); - e.chunk('sys', ['@sys/std']); - }, - }); -}); diff --git a/deploy/tmp.db.team/deno.json b/deploy/tmp.db.team/deno.json index 182f6d7ec1..f121f85398 100644 --- a/deploy/tmp.db.team/deno.json +++ b/deploy/tmp.db.team/deno.json @@ -1,6 +1,6 @@ { "name": "@tdb/tmp", - "version": "0.0.72", + "version": "0.0.82", "tasks": { "dev": "deno run -RNE --watch ./src/main.ts", "check": "deno check ./src/main.ts", diff --git a/deploy/tmp.db.team/src/pkg.ts b/deploy/tmp.db.team/src/pkg.ts index 094f9fce61..d265bd13a2 100644 --- a/deploy/tmp.db.team/src/pkg.ts +++ b/deploy/tmp.db.team/src/pkg.ts @@ -1,8 +1,16 @@ -import { Pkg, type t } from 'jsr:@sys/std'; -import { default as deno } from '../deno.json' with { type: 'json' }; - +import type { Pkg } from '@sys/types'; /** * Package meta-data. + * + * AUTO-GENERATED: + * This file is generated via the `prep` command across the + * @system monorepo. See command: + * + * cd ./<system-repo-root> + * deno task prep + * + * - DO check this file in to source-control. + * - Do NOT manually alter the file (as your work will be lost). */ -export const pkg: t.Pkg = Pkg.fromJson(deno); +export const pkg: Pkg = { name: '@tdb/tmp', version: '0.0.82' }; diff --git a/deps.yaml b/deps.yaml index 9bd188f371..14d4b33669 100644 --- a/deps.yaml +++ b/deps.yaml @@ -3,7 +3,7 @@ # # ./š¦ # | deno.json -# |(write) ā deno.imports.json +# |(write) ā imports.json # |(write) ā package.json # # This is the "single-source-of-truth" with regards to dependencies and versioning. @@ -44,6 +44,7 @@ groups: build/tools: - import: npm:@vitejs/plugin-react-swc@3.8.0 + - import: npm:@deno/vite-plugin@1.0.4 - import: npm:rollup@4.34.8 - import: npm:vite@6.1.1 - import: npm:vite-plugin-wasm@3.4.1 @@ -64,6 +65,9 @@ deno.json: - import: jsr:@cliffy/prompt@1.0.0-rc.7 - import: jsr:@cliffy/table@1.0.0-rc.7 + # Sundry: JSR + - import: jsr:@deno/dnt@0.41.3 + # Sundry: NPM - import: npm:@types/diff@7.0.1 - import: npm:chai@5 @@ -89,15 +93,20 @@ deno.json: - import: npm:ts-essentials@10.0.4 - import: npm:valibot@1.0.0-rc.1 - import: npm:yaml@2.7.0 + - import: npm:@preact/signals-core@1.8.0 # Browser - import: npm:csstype@3 - import: npm:ua-parser-js@2.0.2 # UI + - import: npm:@svgdotjs/svg.js@3.2.4 + # UI:React - import: npm:react-error-boundary@5 - import: npm:react-inspector@6 - import: npm:react-spinners@0.15.0 + - import: npm:@preact/signals-react@3.0.1 + - import: npm:react-inspector@6.0.2 package.json: - group: std/deno @@ -111,6 +120,7 @@ package.json: - group: ui/react - import: npm:react-icons@5.5.0 - import: npm:@vidstack/react@1.12.12 + - import: npm:motion@12.5.0 # UI:Frameworks - import: npm:vitepress@1.6.3 diff --git a/deno.imports.json b/imports.json similarity index 89% rename from deno.imports.json rename to imports.json index 8303ae8b14..4ecdadc967 100644 --- a/deno.imports.json +++ b/imports.json @@ -8,9 +8,12 @@ "@cliffy/keypress": "jsr:@cliffy/keypress@1.0.0-rc.7", "@cliffy/prompt": "jsr:@cliffy/prompt@1.0.0-rc.7", "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.7", + "@deno/dnt": "jsr:@deno/dnt@0.41.3", "@noble/hashes": "npm:@noble/hashes@1.7.1", "@noble/hashes/*": "npm:@noble/hashes@1.7.1/*", "@onsetsoftware/automerge-patcher": "npm:@onsetsoftware/automerge-patcher@0.14.0", + "@preact/signals-core": "npm:@preact/signals-core@1.8.0", + "@preact/signals-react": "npm:@preact/signals-react@3.0.1", "@std/async": "jsr:@std/async@1.0.10", "@std/datetime": "jsr:@std/datetime@0.225.3", "@std/dotenv": "jsr:@std/dotenv@0.225.3", @@ -20,6 +23,7 @@ "@std/semver": "jsr:@std/semver@1.0.4", "@std/testing": "jsr:@std/testing@1.0.9", "@std/uuid": "jsr:@std/uuid@1.0.4", + "@svgdotjs/svg.js": "npm:@svgdotjs/svg.js@3.2.4", "@types/diff": "npm:@types/diff@7.0.1", "approx-string-match": "npm:approx-string-match@2", "chai": "npm:chai@5", @@ -38,7 +42,7 @@ "rambda": "npm:rambda@9.4.2", "ramda": "npm:ramda@0.30.1", "react-error-boundary": "npm:react-error-boundary@5", - "react-inspector": "npm:react-inspector@6", + "react-inspector": "npm:react-inspector@6.0.2", "react-spinners": "npm:react-spinners@0.15.0", "rxjs": "npm:rxjs@7.8.2", "strip-ansi": "npm:strip-ansi@7", diff --git a/package.json b/package.json index 73019e3192..5b7c817292 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@types/react-dom": "18.3.5", "@vidstack/react": "1.12.12", "hono": "4.7.2", + "motion": "12.5.0", "react": "18.3.1", "react-dom": "18.3.1", "react-icons": "5.5.0", @@ -21,6 +22,7 @@ "vue": "3.5.13" }, "devDependencies": { + "@deno/vite-plugin": "1.0.4", "@vitejs/plugin-react-swc": "3.8.0", "rollup": "4.34.8", "vite": "6.1.1", diff --git a/scripts/-tmp.ts b/scripts/-tmp.ts index 5ff92b9f19..5451d7103c 100644 --- a/scripts/-tmp.ts +++ b/scripts/-tmp.ts @@ -1,25 +1,79 @@ -import { Jsr } from '@sys/jsr'; -import { Cli } from '@sys/cli'; -import { Paths } from './u.paths.ts'; - -// console.log('Jsr', Jsr); -// const info = await Jsr.Fetch.Pkg.info('@sys/std'); -// console.log('info', info); -// console.log(`-------------------------------------------`); -// console.log('moduleGraph1', info.data?.moduleGraph1); -// console.log('moduleGraph2', info.data?.moduleGraph2); - -const options = Paths.modules.map((path) => { - return { name: path, value: path }; -}); - -const res = await Cli.Prompt.Checkbox.prompt({ - message: '(Workflow) JSR Publish Modules:', - options, -}); -console.log('res', res); +import { Args, c, Cli } from '@sys/cli'; +import { Fs } from '@sys/fs'; +import { rx, Str } from '@sys/std'; + +const { italic: i } = c; + +type TArgs = { watch?: boolean }; +const args = Args.parse<TArgs>(Deno.args, { boolean: ['watch'], alias: { w: 'watch' } }); +console.info(c.cyan('args:'), args); + +let copyCount = 0; /** - * Finish up. + * Copy the SLC project content to the VitePress (driver) development sample directory + * from it's external content authoring location. */ -Deno.exit(0); +const dir = { + source: '/Users/phil/Documents/Notes/tdb/slc/slc-public', + target: Fs.resolve('code/sys.driver/driver-vitepress/.tmp/sample'), +} as const; + +export async function copyDocs() { + const Fmt = { + path: (path: string) => `${c.gray(Fs.dirname(path))}/${c.white(Fs.basename(path))}`, + } as const; + + copyCount++; + + const title = c.bold(c.brightGreen('Copy')); + const table = Cli.table([title]); + const push = (label: string, value: string) => table.push([c.gray(label), value]); + const blankLine = () => table.push([]); + + const copy = async (from: string, to: string) => { + from = Fs.join(dir.source, from); + to = Fs.join(dir.target, to); + await Fs.copy(from, to, { force: true }); + push(` ⢠from`, c.gray(Fs.trimCwd(from))); + push(` ⢠${c.cyan('to')}`, Fmt.path(Fs.trimCwd(to))); + blankLine(); + }; + + blankLine(); + await copy('docs', 'docs'); + await copy('src', 'src'); + + // Ouput. + console.info(); + console.info(table.toString().trim()); + console.info(); + console.info(i(`copied ${c.green(String(copyCount))} ${Str.plural(copyCount, 'time', 'times')}`)); +} + +await copyDocs(); + +/** + * Watcher. + */ +if (args.watch) { + const watcher = await Fs.watch(dir.source); + watcher.$.pipe(rx.debounceTime(1000)).subscribe(copyDocs); + console.info(); + console.info(i(c.gray(` (watching for file changes)`))); + console.info(i(c.gray(` ${c.yellow('Enter')} to force copy`))); + console.info(); + + for await (const e of Cli.keypress()) { + if (e.key === 'return') copyDocs(); + if ((e.ctrlKey && e.key === 'c') || e.key === 'q') Deno.exit(0); + } +} + +// Finish up. +if (!args.watch) { + const y = c.yellow; + console.info(c.italic(c.gray(`(pass ${y('--watch')} (${y('-w')}) to re-run on file changes)`))); + console.info(); + Deno.exit(0); +} diff --git a/scripts/Task.-bump.ts b/scripts/Task.-bump.ts index 55d212dc60..c2670a7d26 100644 --- a/scripts/Task.-bump.ts +++ b/scripts/Task.-bump.ts @@ -36,13 +36,13 @@ export async function main(options: Options = {}) { * Retrieve the child modules within the workspace. */ const children = ws.children - .filter((child) => !exclude(child.path)) - .filter((child) => !!child.file.version) - .filter((child) => typeof child.file.version === 'string') - .filter((child) => typeof child.file.name === 'string') + .filter((child) => !exclude(child.path.denofile)) + .filter((child) => !!child.denofile.version) + .filter((child) => typeof child.denofile.version === 'string') + .filter((child) => typeof child.denofile.name === 'string') .map((child) => { - const json = child.file; - const path = child.path; + const json = child.denofile; + const path = child.path.denofile; const { name = '', version = '' } = json; const current = Semver.parse(version).version; const next = wrangle.increment(current, release); @@ -59,9 +59,9 @@ export async function main(options: Options = {}) { const pkg = `${c.gray(modScope)}/${c.white(c.bold(modName))}`; const vCurrent = Semver.toString(version.current); - const vNext = Semver.Fmt.colorize(version.next, { highlight: release, baseColor: c.green }); + const vNext = Semver.Fmt.colorize(version.next, { highlight: release }); - const title = `${c.green(' ā¢')} ${pkg}`; + const title = `${c.cyan(' ā¢')} ${pkg}`; table.push([title, vCurrent, 'ā', vNext]); }); diff --git a/scripts/Task.-dry.ts b/scripts/Task.-dry.ts index 1066efaf9e..6f7e4fd460 100644 --- a/scripts/Task.-dry.ts +++ b/scripts/Task.-dry.ts @@ -6,11 +6,14 @@ export async function main() { const results: CmdResult[] = []; const run = async (path: string, index: number, total: number) => { - const command = `deno task dry`; + const cmd = 'dry'; + const command = `deno task ${cmd}`; + const commandFmt = c.green(`deno task ${c.bold(c.cyan(cmd))}`); + const title = c.gray(`${c.white('Type Checks')} (${c.white(String(index + 1))} of ${total})`); const moduleList = Log.moduleList({ index, indent: 3 }); - const text = `${title}\n ${c.cyan(command)}\n${moduleList}`; + const text = `${title}\n ${commandFmt}\n${moduleList}`; spinner.text = c.gray(text); const output = await Process.sh(path).run(command); diff --git a/scripts/Task.-info.ts b/scripts/Task.-info.ts index 2aa5f8261b..600639204b 100644 --- a/scripts/Task.-info.ts +++ b/scripts/Task.-info.ts @@ -11,7 +11,7 @@ export async function main() { const process = async (path: string) => { const fileInfo = await Deno.stat(path); if (fileInfo.isFile) { - const file = (await Fs.readText(path)).data; + const file = (await Fs.readText(path)).data ?? ''; const lines = file.split('\n'); files.push({ path, total: { lines: lines.length } }); } diff --git a/scripts/Task.-prep.ts b/scripts/Task.-prep.ts index 632af83a77..46316a1d09 100644 --- a/scripts/Task.-prep.ts +++ b/scripts/Task.-prep.ts @@ -1,18 +1,20 @@ -import { c, DenoDeps, Fs, Process } from './common.ts'; +import { type t, c, DenoDeps, DenoFile, Err, Fs, Process, Tmpl } from './common.ts'; const i = c.italic; /** - * Prepare the [deno.json | package.json] files from - * definitions within the monorepo's `deps.yaml` configuration. + * Proecss the dependencies into a`deno.json` and `package.json` files. */ -export async function main() { +async function processDeps() { const res = await DenoDeps.from('./deps.yaml'); - if (res.error) return console.error(res.error); + if (res.error) { + console.error(res.error); + return; + } const PATH = { package: './package.json', - deno: './deno.imports.json', - }; + deno: './imports.json', + } as const; /** * Write to file-system: [deno.json | package.json]. @@ -21,16 +23,6 @@ export async function main() { await Fs.writeJson(PATH.package, DenoDeps.toJson('package.json', deps)); await Fs.writeJson(PATH.deno, DenoDeps.toJson('deno.json', deps)); - /** - * Run `prep` ā `init` commands on sub-modules. - */ - const sh = (path: string) => Process.sh({ path }); - const module = (...parts: string[]) => sh(Fs.resolve('./code', ...parts)); - - const cmd = 'deno task prep && deno task init'; - await module('sys.driver/driver-vite').run(cmd); - await module('sys.driver/driver-vitepress').run(cmd); - /** * Output: console. */ @@ -43,3 +35,65 @@ export async function main() { console.info(DenoDeps.Fmt.deps(deps, { indent: 1 })); console.info(); } + +/** + * Write all {pkg}.ts files with name/version values synced + * to their corresponding current `deno.json` file values. + */ +async function updatePackages() { + const errors = Err.errors(); + const ws = await DenoFile.workspace(); + + const tmpl = Tmpl.create('./code/-tmpl/pkg', async (e) => { + const pkg = e.ctx?.pkg as t.Pkg; + if (typeof pkg !== 'object') { + const err = `[UpdatePackages] Template expected a {pkg} on the context. Module: ${e.tmpl.absolute}`; + errors.push(err); + return; + } + + if (e.target.file.name === 'pkg.ts') { + const text = e.text.tmpl.replace(/<NAME>/, pkg.name).replace(/<VERSION>/, pkg.version); + e.modify(text); + } + }); + + for (const item of ws.children) { + const targetDir = Fs.join(item.path.dir, 'src'); + const exists = await Fs.exists(Fs.join(targetDir, 'pkg.ts')); + if (exists) { + const pkg = item.pkg; + const ctx = { pkg }; + await tmpl.write(targetDir, { ctx }); + } + } + + const error = errors.toError(); + if (error) console.error(error); + return { error }; +} + +/** + * Run `prep` ā `init` commands on sub-modules. + */ +async function prepSubmodules() { + const ws = await DenoFile.workspace(); + for (const item of ws.children) { + const tasks = item.denofile.tasks; + if (tasks) { + const sh = Process.sh(item.path.dir); + if (tasks.prep) await sh.run('deno task prep'); + if (tasks.init) await sh.run('deno task init'); + } + } +} + +/** + * Prepare the [deno.json | package.json] files from + * definitions within the monorepo's `deps.yaml` configuration. + */ +export async function main() { + await processDeps(); + await updatePackages(); + await prepSubmodules(); +} diff --git a/scripts/Task.-test.ts b/scripts/Task.-test.ts index 44a5545dca..89a1438141 100644 --- a/scripts/Task.-test.ts +++ b/scripts/Task.-test.ts @@ -9,11 +9,14 @@ export async function main() { */ const results: CmdResult[] = []; const run = async (path: string, index: number, total: number) => { - const command = `deno task test`; + const cmd = 'test'; + const command = `deno task ${cmd}`; + const commandFmt = c.green(`deno task ${c.bold(c.cyan(cmd))}`); + const title = c.gray(`${c.white('Tests')} (${c.white(String(index + 1))} of ${total})`); const moduleList = Log.moduleList({ index, indent: 3 }); - spinner.text = c.gray(`${title}\n ${c.cyan(command)}\n${moduleList}`); - const output = await Process.sh({ silent: true, path }).run(command); + spinner.text = c.gray(`${title}\n ${commandFmt}\n${moduleList}`); + const output = await Process.sh({ path, silent: true }).run(command); results.push({ output, path }); }; diff --git a/scripts/Task.-tmpl.ts b/scripts/Task.-tmpl.ts new file mode 100644 index 0000000000..4e615832a8 --- /dev/null +++ b/scripts/Task.-tmpl.ts @@ -0,0 +1,65 @@ +import { c, Cli, Fs, Tmpl } from './common.ts'; + +type Options = { + argv?: string[]; +}; + +const Templates = { + 'm.mod': 'code/-tmpl/m.mod/', + 'm.mod.ui': 'code/-tmpl/m.mod.ui/', + 'pkg.deno': 'code/-tmpl/deno/', +} as const; + +type TArgs = { + tmpl?: string | boolean; +}; + +/** + * COMMAND š³ Create from template action. + */ +export async function main(options: Options = {}) { + const args = Cli.args<TArgs>(options.argv ?? Deno.args); + console.info(c.gray('args:'), args); + console.info(); + + let name = typeof args.tmpl === 'string' ? args.tmpl : ''; + const templates = Object.keys(Templates); + + if (!name) { + name = await Cli.Prompt.Select.prompt({ + message: 'Select Template:', + options: templates.map((name: string) => ({ name, value: name })), + }); + } + + if (!templates.includes(name)) { + const msg = `${c.yellow('Failed:')} A template named "${c.white(name)}" does not exist.`; + console.info(); + console.warn(c.gray(msg)); + console.info(c.gray(c.italic('(pass nothing for interactive list)'))); + console.info(); + return; + } + + const dirname = await Cli.Prompt.Input.prompt('Directory Name:'); + const targetDir = Fs.join(Fs.cwd('init'), dirname); + + if (await Fs.exists(targetDir)) { + const noChange = c.green('No Change'); + const msg = `${c.yellow('Warning:')} Something already exists at that location (${noChange}).`; + console.info(); + console.warn(c.gray(msg)); + console.warn(c.gray(targetDir)); + console.info(); + return; + } + + const sourceDir = Fs.resolve(Templates[name as keyof typeof Templates]); + const tmpl = Tmpl.create(sourceDir); + const res = await tmpl.write(targetDir); + + console.info(c.gray(`Target: ${Fs.trimCwd(targetDir)}`)); + console.info(); + console.info(Tmpl.Log.table(res.ops)); + console.info(); +} diff --git a/scripts/common.ts b/scripts/common.ts index b78185f6d9..55e4577d6e 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -1,10 +1,11 @@ -export { R, Value } from '@sys/std'; +export { Err, R, Value } from '@sys/std'; export { Semver } from '@sys/std/semver/server'; export { c, Cli } from '@sys/cli'; +export { DenoDeps, DenoFile } from '@sys/driver-deno/runtime'; export { Fs } from '@sys/fs'; export { Process } from '@sys/process'; -export { DenoDeps } from '@sys/driver-deno/runtime'; +export { Tmpl } from '@sys/tmpl/fs'; export * as t from './t.ts'; export { Path, Paths } from './u.paths.ts'; diff --git a/scripts/main.ts b/scripts/main.ts index e17b8172bb..ea625b3063 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -7,28 +7,35 @@ import { main as info } from './Task.-info.ts'; import { main as lint } from './Task.-lint.ts'; import { main as prep } from './Task.-prep.ts'; import { main as test } from './Task.-test.ts'; +import { main as tmpl } from './Task.-tmpl.ts'; type T = { dry?: boolean; test?: boolean; - clean?: boolean; info?: boolean; + + clean?: boolean; lint?: boolean; bump?: boolean; prep?: boolean; + + tmpl?: boolean; }; const args = Cli.args<T>(Deno.args); -// Maintenance. +// CI: +if (args.dry) await dry(); +if (args.test) await test(); +if (args.info) await info(); + +// Maintenance: if (args.clean) await clean(); if (args.lint) await lint(); if (args.bump) await bump(); if (args.prep) await prep(); -// CI. -if (args.dry) await dry(); -if (args.test) await test(); -if (args.info) await info(); +// Development: +if (args.tmpl) await tmpl(); // Finish up. Deno.exit(0); diff --git a/scripts/t.ts b/scripts/t.ts index 7c9f8f93d4..2dc734b4f2 100644 --- a/scripts/t.ts +++ b/scripts/t.ts @@ -1,3 +1,3 @@ export type * from '@sys/types/t'; export type { ProcOutput } from '@sys/process/t'; -export type { DenoFileJson } from '@sys/driver-deno/t'; +export type { DenoFileJson, DenoWorkspace } from '@sys/driver-deno/t'; diff --git a/scripts/u.paths.ts b/scripts/u.paths.ts index 13191b843e..8216613e90 100644 --- a/scripts/u.paths.ts +++ b/scripts/u.paths.ts @@ -5,7 +5,7 @@ export const Paths = { workspace: denojson.workspace, modules: [ /** - * @sys: standard libs. + * @sys: standard-libs: */ 'code/sys/types', 'code/sys/std', @@ -29,7 +29,7 @@ export const Paths = { 'code/sys.tmp', /** - * UI + * UI: */ 'code/sys.ui/ui-css', 'code/sys.ui/ui-dom', @@ -38,7 +38,7 @@ export const Paths = { 'code/sys.ui/ui-react-components', /** - * Drivers + * Drivers: */ // 'code/sys.driver/driver-automerge', 'code/sys.driver/driver-deno', @@ -53,9 +53,14 @@ export const Paths = { 'code/sys.driver/driver-quilibrium', /** - * Barrels + * Barrels: */ 'code/sys/sys', 'code/sys/main', + + /** + * Instance Apps: + */ + 'deploy/@tdb.slc', ], } as const;