Styling of home

This commit is contained in:
Sebastian Seedorf
2022-08-22 17:04:39 +02:00
parent 537a18fb88
commit d964748a66
55 changed files with 1791 additions and 341 deletions

View File

@@ -1,35 +1,39 @@
.span {
background: lightgray;
font-size: 1em;
border: 1px solid white;
display: inline-block;
position: relative;
padding-inline: 0.2em;
display: inline-block;
border: 1px solid var(--md-sys-color-outline);
background: var(--md-sys-color-surface-variant);
border-radius: 4px;
color: var(--md-sys-color-on-surface-variant);
font-size: 1em;
padding-inline: 0.2em;
}
.spanSimple {
padding-inline-start: 0.2em;
position: relative;
display: inline-block;
padding-inline-start: 0.2em;
}
.tooltip {
--background: lightsalmon;
--arrow-width: 0.6em;
--arrow-height: 0.4em;
display: none;
--background: var(--md-sys-color-primary-container);
position: absolute;
left: calc(100% + var(--arrow-width));
top: -5000%;
bottom: -5000%;
margin: auto 0;
z-index: 1;
display: none;
width: max-content;
height: max-content;
background: var(--background);
padding: 0.5em;
margin: auto 0;
background: var(--background);
border-radius: 0.7em;
z-index: 1;
box-shadow: 0 0 6px 1px var(--md-sys-color-shadow);
color: var(--md-sys-color-on-primary-container);
inset-block: -5000%;
inset-inline-start: calc(100% + var(--arrow-width));
--arrow-width: 0.6em;
--arrow-height: 0.4em;
}
.span:is(:focus, :hover) > .tooltip {
@@ -37,16 +41,15 @@
}
.tooltip::before {
content: "";
border-style: solid;
top: 0;
bottom: 0;
margin: auto 0;
position: absolute;
height: max-content;
border-width: var(--arrow-height) var(--arrow-width) var(--arrow-height) 0;
border-style: solid;
border-color: transparent var(--background) transparent transparent;
position: absolute;
left: calc(var(--arrow-width) * -1 + 1px);
margin: auto 0;
content: "";
inset-block: 0;
inset-inline-start: calc(var(--arrow-width) * -1 + 1px);
}
.img {
@@ -58,11 +61,11 @@
}
.base {
color: darkgreen;
color: var(--md-sys-color-tertiary);
}
.produced {
color: darkgoldenrod;
color: var(--md-ref-palette-primary70);
}
.unknown {
@@ -89,32 +92,9 @@
.rightClick {
height: 1em;
transform: scaleX(-1) translateY(0.1em);
transform: scaleX(-1) translateY(0.1em) !important;
}
.clickBtn {
fill: red;
}
@media (prefers-color-scheme: dark) {
.span {
border-color: #111111;
background-color: #444;
}
.base {
color: lightgreen;
}
.produced {
color: lightsalmon;
}
.unknown {
color: lightgray;
}
.tooltip {
--background: darkred;
}
}

View File

@@ -1,24 +1,53 @@
.root {
position: relative;
border: 2px solid black;
padding: 1em 0.5em;
border: 1px solid var(--md-sys-color-outline);
background-color: var(--md-sys-color-surface);
border-radius: 4px;
}
.heading {
display: inline-block;
margin-block-start: 0;
}
.heading:not(:focus-visible)::after {
display: inline-block;
width: 1em;
height: 1em;
background-color: var(--md-sys-color-on-background);
content: "";
mask: url("/factorio/pencil.svg") no-repeat 50% 50%;
opacity: 0.8;
padding-inline-start: 0.5em;
}
.quit {
--color: darkred;
--color: var(--md-sys-color-error);
position: absolute;
right: 1em;
top: 1em;
border-radius: 999999px;
background: transparent;
border: 1px solid var(--color);
background: transparent;
border-radius: 999999px;
color: var(--color);
font-weight: 700;
font-weight: 500;
inset-block-start: 1em;
inset-inline-end: 1em;
}
.quit:is(:focus, :hover) {
--color: red;
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.5));
--color: var(--md-ref-palette-error60);
filter: drop-shadow(0 0 2px var(--md-ref-palette-neutral-variant70));
}
.label {
display: block;
}
.marginTop {
display: block;
margin-block-start: 1em;
}
.flex {
@@ -30,18 +59,3 @@
.flex > * {
width: max-content;
}
@media (prefers-color-scheme: dark) {
.root {
border-color: gray;
}
.quit {
--color: indianred;
}
.quit:is(:focus, :hover) {
--color: red;
filter: drop-shadow(0px 0px 2px rgba(255, 255, 255, 0.5));
}
}

View File

@@ -5,13 +5,14 @@ import styles from './GroupBox.module.css'
import { EntitySpan } from '../EntitySpan/EntitySpan'
import { useGroups } from '../../contexts/GroupProvider'
import { calculateInputs } from '../../../src/calculateInputs'
import { fixedEncodeURIComponent, uniquify } from '../../../src/utils'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { uniquify } from '../../../src/utils'
import { useFactories } from '../../contexts/FactoryProvider'
import { i18n, I18n } from '../../shared/I18n/I18n'
import { useIntl } from 'react-intl'
import { GraphIcon } from '../../icons/GraphIcon'
import { ButtonVisualize } from '../../shared/ButtonVisualize/ButtonVisualize'
import { Heading } from '../../shared/Heading'
import typography from '../../../styles/typography.module.css'
import cx from 'classnames'
interface Props {
group: Group
@@ -19,7 +20,6 @@ interface Props {
const GroupBoxBase: FC<Props> = ({ group }) => {
const intl = useIntl()
const { query } = useRouter()
const { factories, findFactory } = useFactories()
const {
doNotSuggest,
@@ -33,6 +33,7 @@ const GroupBoxBase: FC<Props> = ({ group }) => {
getInputType
} = useGroups()
const { name, exports, malls } = group
const nameForId = useMemo(() => name.replace(/[^a-z\d_-]/gi, ''), [name])
const [isDeleteConfirm, setDeleteConfirm] = useState(false)
@@ -79,26 +80,30 @@ const GroupBoxBase: FC<Props> = ({ group }) => {
return (
<div className={styles.root}>
<h3
<ButtonVisualize groupId={group.name} />
<Heading
type={'subsection'}
className={styles.heading}
contentEditable={true}
suppressContentEditableWarning={true}
onBlur={event => {
event.currentTarget.innerText = event.currentTarget.innerText.trim()
renameGroup(name, event.currentTarget.innerText)
}}
>
{name}
</h3>
<Link
href={{
pathname: `/visualize/${fixedEncodeURIComponent(group.name)}`,
query
onKeyDown={event => {
if (event.key === 'Enter') {
event.preventDefault()
event.currentTarget.blur()
}
if (event.key === 'Escape') {
event.preventDefault()
event.currentTarget.innerText = name
event.currentTarget.blur()
}
}}
>
<a>
<GraphIcon />
</a>
</Link>
{name}
</Heading>
<button
className={styles.quit}
onBlur={() => setDeleteConfirm(false)}
@@ -107,23 +112,31 @@ const GroupBoxBase: FC<Props> = ({ group }) => {
>
{isDeleteConfirm ? i18n(intl, 'page.home.group.delete.confirmation') : 'X'}
</button>
<h4>
<I18n id={'page.home.group.item.export'} />
</h4>
<FactorySelect
id={name + '-exports'}
factories={exports}
onSetFactories={setExportFactories}
/>
<h4>
<I18n id={'page.home.group.item.mall'} />
</h4>
<FactorySelect id={name + '-malls'} factories={malls} onSetFactories={setMallFactories} />
<label htmlFor={nameForId + '-exports'} className={styles.label}>
<span className={typography.titleMedium}>
<I18n id={'page.home.group.item.export'} />
</span>
<FactorySelect
id={nameForId + '-exports'}
factories={exports}
onSetFactories={setExportFactories}
/>
</label>
<label htmlFor={nameForId + '-exports'} className={cx(styles.label, styles.marginTop)}>
<span className={typography.titleMedium}>
<I18n id={'page.home.group.item.mall'} />
</span>
<FactorySelect
id={nameForId + '-malls'}
factories={malls}
onSetFactories={setMallFactories}
/>
</label>
{inputs.length ? (
<>
<h4>
<span className={cx(typography.titleMedium, styles.marginTop)}>
<I18n id={'page.home.group.item.input'} values={{ amount: inputs.length }} />
</h4>
</span>
<div className={styles.flex}>
{inputs.map(input => (
<EntitySpan
@@ -142,12 +155,12 @@ const GroupBoxBase: FC<Props> = ({ group }) => {
) : null}
{intermediates.length ? (
<>
<h4>
<span className={cx(typography.titleMedium, styles.marginTop)}>
<I18n
id={'page.home.group.item.intermediate'}
values={{ amount: intermediates.length }}
/>
</h4>
</span>
<div className={styles.flex}>
{intermediates.map(intermediate => (
<EntitySpan
@@ -161,9 +174,9 @@ const GroupBoxBase: FC<Props> = ({ group }) => {
) : null}
{suggestionsExport.length ? (
<>
<h4>
<span className={cx(typography.titleMedium, styles.marginTop)}>
<I18n id={'page.home.group.item.suggestion.export'} />
</h4>
</span>
<div className={styles.flex}>
{suggestionsExport.map(suggestion => (
<EntitySpan
@@ -183,9 +196,9 @@ const GroupBoxBase: FC<Props> = ({ group }) => {
) : null}
{suggestionMall.length ? (
<>
<h4>
<span className={cx(typography.titleMedium, styles.marginTop)}>
<I18n id={'page.home.group.item.suggestion.mall'} />
</h4>
</span>
<div className={styles.flex}>
{suggestionMall.map(suggestion => (
<EntitySpan

View File

@@ -1,17 +1,17 @@
.content {
max-width: 80ch;
margin: 0 auto;
text-align: justify;
}
.grid {
display: grid;
margin-top: 2em;
gap: 1em;
grid-template-columns: repeat(auto-fit, minmax(450px, max-content));
margin-block-start: -2em;
}
.missingFactories {
display: flex;
flex-wrap: wrap;
gap: 0.1em;
}
.missingFactories > * {
.entitySpanList > * {
width: max-content;
}

View File

@@ -1,125 +1,38 @@
import { ElementRef, FC, useMemo, useRef } from 'react'
import { FC } from 'react'
import { GroupBox } from './GroupBox/GroupBox'
import styles from './Home.module.css'
import { EnrichedEntity } from '../../src/types'
import { EntitySpan } from './EntitySpan/EntitySpan'
import { useGroups } from '../contexts/GroupProvider'
import { Preferences } from './Preferences/Preferences'
import { download, streamToArrayBuffer } from '../../src/download'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { i18n, I18n } from '../shared/I18n/I18n'
import { useFactories } from '../contexts/FactoryProvider'
import { GraphIcon } from '../icons/GraphIcon'
import { useIntl } from 'react-intl'
import { SectionPreferences } from './SectionPreferences/SectionPreferences'
import { I18n } from '../shared/I18n/I18n'
import { Heading } from '../shared/Heading'
import { Paragraph } from '../shared/Paragraph/Paragraph'
import { Section } from '../shared/Section/Section'
import { SectionAddMissing } from './SectionAddMissing/SectionAddMissing'
import { SectionShare } from './SectionShare/SectionShare'
import { ButtonVisualize } from '../shared/ButtonVisualize/ButtonVisualize'
export const Home: FC = () => {
const intl = useIntl()
const { query } = useRouter()
const { factories } = useFactories()
const preferencesRef = useRef<ElementRef<typeof Preferences>>(null)
const { groups, doNotSuggest, ignoredFactories, setIgnoredFactories, store, load } = useGroups()
const inputRef = useRef<HTMLInputElement>(null)
const [missingExport, missingMall] = useMemo<[EnrichedEntity[], EnrichedEntity[]]>(() => {
return factories
.filter(factory => !doNotSuggest.has(factory.href) && factory.recipe)
.reduce(
(acc, factory) =>
(factory.usedBy?.length ?? 0) >= 3
? [[...acc[0], factory], acc[1]]
: [acc[0], [...acc[1], factory]],
[[], []] as [EnrichedEntity[], EnrichedEntity[]]
)
}, [factories, doNotSuggest])
const { groups } = useGroups()
return (
<main>
<h1>
<I18n id={'page.home.title'} />
</h1>
<p>
<I18n id={'page.home.description'} />
</p>
<button
onClick={() => {
download('factorio-microservices.bin', store())
}}
>
<I18n id={'page.home.pref.download'} />
</button>
<input
type={'file'}
multiple={false}
ref={inputRef}
onChange={async evt => {
const stream = evt.currentTarget.files?.[0].stream() as
| globalThis.ReadableStream<Uint8Array>
| undefined
if (stream) {
const array = await streamToArrayBuffer(stream)
load(array)
if (inputRef.current) inputRef.current.value = null as unknown as string
}
}}
/>
<Link href={{ pathname: '/visualize', query }}>
<a>
<I18n id={'page.home.pref.visualize'} />
<GraphIcon />
</a>
</Link>
<Preferences ref={preferencesRef} />
<fieldset>
<legend>
<I18n id={'page.home.group.missing.export.title'} />
</legend>
<span>
<I18n id={'page.home.group.missing.export.description'} />
</span>
<div className={styles.missingFactories}>
{missingExport.map(missing => (
<EntitySpan
key={missing.href}
value={missing}
onClick={() => {
preferencesRef.current?.addGroup(missing.name, [missing.href])
}}
onContextMenu={event => {
event.preventDefault()
setIgnoredFactories([...ignoredFactories, missing.href])
}}
leftClickText={i18n(intl, 'page.home.tooltip.action.add_to_new_export')}
rightClickText={i18n(intl, 'page.home.tooltip.action.exclude_suggestion')}
/>
))}
</div>
</fieldset>
<fieldset>
<legend>
<I18n id={'page.home.group.missing.mall.title'} />
</legend>
<span>
<I18n id={'page.home.group.missing.mall.description'} />
</span>
<div className={styles.missingFactories}>
{missingMall.map(missing => (
<EntitySpan
key={missing.href}
value={missing}
onClick={() => {
preferencesRef.current?.addGroup(missing.name, undefined, [missing.href])
}}
onContextMenu={event => {
event.preventDefault()
setIgnoredFactories([...ignoredFactories, missing.href])
}}
leftClickText={i18n(intl, 'page.home.tooltip.action.add_to_new_mall')}
rightClickText={i18n(intl, 'page.home.tooltip.action.exclude_suggestion')}
/>
))}
</div>
</fieldset>
<Section>
<Heading type={'pageTitle'}>
<I18n id={'page.home.title'} />
</Heading>
<Paragraph size={'large'}>
<I18n id={'page.home.description'} />
</Paragraph>
</Section>
<SectionShare />
<SectionPreferences />
<SectionAddMissing />
<Section>
<Heading type={'section'}>
<I18n id={'page.home.group.title'} />
<ButtonVisualize large={true} />
</Heading>
</Section>
<div className={styles.grid}>
{Object.values(groups)
.sort((a, b) => a.name.localeCompare(b.name))

View File

@@ -1,74 +0,0 @@
import { forwardRef, ForwardRefRenderFunction, useImperativeHandle, useState } from 'react'
import { FactorySelect } from '../FactorySelect/FactorySelect'
import { useGroups } from '../../contexts/GroupProvider'
import { i18n, I18n } from '../../shared/I18n/I18n'
import { useIntl } from 'react-intl'
interface Handle {
addGroup(preferredName: string, exported?: string[], mall?: string[]): boolean
}
const PreferencesBase: ForwardRefRenderFunction<Handle> = (_, forwardedRef) => {
const intl = useIntl()
const DEFAULT_NAME = i18n(intl, 'page.home.group.add.default_group_name')
useImperativeHandle(forwardedRef, () => ({
addGroup(preferredName: string, exported?: string[], mall?: string[]) {
const name = newGroupValue !== DEFAULT_NAME ? newGroupValue : preferredName
const result = addGroup(name, exported, mall)
result && setNewGroupValue(DEFAULT_NAME)
return result
}
}))
const { addGroup, baseFactories, setBaseFactories, ignoredFactories, setIgnoredFactories } =
useGroups()
const [newGroupValue, setNewGroupValue] = useState(DEFAULT_NAME)
return (
<>
<fieldset>
<legend>
<I18n id={'page.home.pref.basic.title'} />
</legend>
<span>
<I18n id={'page.home.pref.basic.description'} />
</span>
<FactorySelect
id={'baseFactoriesSelect'}
factories={baseFactories}
onSetFactories={setBaseFactories}
fixInputs={true}
/>
</fieldset>
<fieldset>
<legend>
<I18n id={'page.home.pref.ignored.title'} />
</legend>
<span>
<I18n id={'page.home.pref.ignored.description'} />
</span>
<FactorySelect
id={'ignoredFactoriesSelect'}
factories={ignoredFactories}
onSetFactories={setIgnoredFactories}
/>
</fieldset>
<fieldset>
<legend>
<I18n id={'page.home.group.add.title'} />
</legend>
<input value={newGroupValue} onChange={e => setNewGroupValue(e.target.value)} />
<button
disabled={!newGroupValue}
onClick={() => {
addGroup(newGroupValue)
setNewGroupValue(DEFAULT_NAME)
}}
>
<I18n id={'page.home.group.add.button_text'} values={{ name: newGroupValue }} />
</button>
</fieldset>
</>
)
}
export const Preferences = forwardRef(PreferencesBase)

View File

@@ -2,6 +2,7 @@ import { FC } from 'react'
import { Recipe } from '../../../src/types'
import { EntityIcon } from '../EntityIcon/EntityIcon'
import styles from './Recipe.module.css'
import { I18n } from '../../shared/I18n/I18n'
interface Props {
recipe: Recipe
@@ -23,7 +24,8 @@ export const RecipeSpan: FC<Props> = ({ recipe }) => {
const after = Object.entries({ ...recipe.output }).map(toEntityIcon)
return (
<span className={styles.recipe}>
{joinByPlus([toEntityIcon(['/Time', recipe.time]), ...before])} {joinByPlus(after)}
{joinByPlus([toEntityIcon(['/Time', recipe.time]), ...before])}{' '}
<I18n id={'component.recipe.arrow'} /> {joinByPlus(after)}
</span>
)
}

View File

@@ -0,0 +1,9 @@
.addBtn {
margin-inline: 1em;
}
.entitySpanList {
display: flex;
flex-wrap: wrap;
gap: 0.1em;
}

View File

@@ -0,0 +1,123 @@
import { FC, useCallback, useMemo, useState } from 'react'
import { Heading } from '../../shared/Heading'
import { i18n, I18n } from '../../shared/I18n/I18n'
import { Paragraph } from '../../shared/Paragraph/Paragraph'
import styles from './SectionAddMissing.module.css'
import { EntitySpan } from '../EntitySpan/EntitySpan'
import { Section } from '../../shared/Section/Section'
import { EnrichedEntity } from '../../../src/types'
import { useFactories } from '../../contexts/FactoryProvider'
import { useGroups } from '../../contexts/GroupProvider'
import { useIntl } from 'react-intl'
import { Input } from '../../shared/Input/Input'
import { Button } from '../../shared/Button/Button'
export const SectionAddMissing: FC = () => {
const intl = useIntl()
const DEFAULT_NAME = useMemo(() => i18n(intl, 'page.home.group.add.default_group_name'), [intl])
const { factories } = useFactories()
const { addGroup: addGroupCtx, doNotSuggest, ignoredFactories, setIgnoredFactories } = useGroups()
const [newGroupValue, setNewGroupValue] = useState(DEFAULT_NAME)
const addGroup = useCallback(
(preferredName: string, exported?: string[], mall?: string[]) => {
const name = newGroupValue !== DEFAULT_NAME ? newGroupValue : preferredName
const result = addGroupCtx(name, exported, mall)
result && setNewGroupValue(DEFAULT_NAME)
return result
},
[DEFAULT_NAME, addGroupCtx, newGroupValue]
)
const [missingExport, missingMall] = useMemo<[EnrichedEntity[], EnrichedEntity[]]>(() => {
return factories
.filter(factory => !doNotSuggest.has(factory.href) && factory.recipe)
.reduce(
(acc, factory) =>
(factory.usedBy?.length ?? 0) >= 3
? [[...acc[0], factory], acc[1]]
: [acc[0], [...acc[1], factory]],
[[], []] as [EnrichedEntity[], EnrichedEntity[]]
)
}, [factories, doNotSuggest])
return (
<Section color={'secondary'}>
<Heading type={'section'}>
<I18n id={'page.home.group.add.title'} />
</Heading>
<Input value={newGroupValue} onChange={e => setNewGroupValue(e.target.value)} />
<Button
className={styles.addBtn}
disabled={!newGroupValue}
onClick={() => {
addGroup(newGroupValue)
setNewGroupValue(DEFAULT_NAME)
}}
>
<I18n id={'page.home.group.add.button_text'} values={{ name: newGroupValue }} />
</Button>
<Heading type={'subsection'}>
<I18n id={'page.home.group.missing.export.title'} />
</Heading>
<Paragraph size={'subtitle'}>
<I18n id={'page.home.group.missing.export.description'} />
</Paragraph>
{missingExport.length ? (
<div className={styles.entitySpanList}>
{missingExport.map(missing => (
<EntitySpan
key={missing.href}
value={missing}
onClick={() => {
addGroup(missing.name, [missing.href])
}}
onContextMenu={event => {
event.preventDefault()
setIgnoredFactories([...ignoredFactories, missing.href])
}}
leftClickText={i18n(intl, 'page.home.tooltip.action.add_to_new_export')}
rightClickText={i18n(intl, 'page.home.tooltip.action.exclude_suggestion')}
/>
))}
</div>
) : (
<Paragraph size={'medium'}>
<em>
<I18n id={'page.home.group.missing.none'} />
</em>
</Paragraph>
)}
<Heading type={'subsection'}>
<I18n id={'page.home.group.missing.mall.title'} />
</Heading>
<Paragraph size={'subtitle'}>
<I18n id={'page.home.group.missing.mall.description'} />
</Paragraph>
{missingMall.length ? (
<div className={styles.entitySpanList}>
{missingMall.map(missing => (
<EntitySpan
key={missing.href}
value={missing}
onClick={() => {
addGroup(missing.name, undefined, [missing.href])
}}
onContextMenu={event => {
event.preventDefault()
setIgnoredFactories([...ignoredFactories, missing.href])
}}
leftClickText={i18n(intl, 'page.home.tooltip.action.add_to_new_mall')}
rightClickText={i18n(intl, 'page.home.tooltip.action.exclude_suggestion')}
/>
))}
</div>
) : (
<Paragraph size={'medium'}>
<em>
<I18n id={'page.home.group.missing.none'} />
</em>
</Paragraph>
)}
</Section>
)
}

View File

@@ -0,0 +1,3 @@
.margin-top {
margin-block-start: 2em;
}

View File

@@ -0,0 +1,45 @@
import { FC } from 'react'
import { FactorySelect } from '../FactorySelect/FactorySelect'
import { useGroups } from '../../contexts/GroupProvider'
import { I18n } from '../../shared/I18n/I18n'
import { Section } from '../../shared/Section/Section'
import { Heading } from '../../shared/Heading'
import { Paragraph } from '../../shared/Paragraph/Paragraph'
import { Collapsible } from '../../shared/Collapsible/Collapsible'
import styles from './SectionPreferences.module.css'
export const SectionPreferences: FC = () => {
const { baseFactories, setBaseFactories, ignoredFactories, setIgnoredFactories } = useGroups()
return (
<Section>
<Collapsible id={'collapseBase'}>
<Heading type={'section'}>
<I18n id={'page.home.pref.basic.title'} />
</Heading>
<Paragraph size={'subtitle'}>
<I18n id={'page.home.pref.basic.description'} />
</Paragraph>
<FactorySelect
id={'baseFactoriesSelect'}
factories={baseFactories}
onSetFactories={setBaseFactories}
fixInputs={true}
/>
</Collapsible>
<Collapsible id={'collapseIgnored'} className={styles.marginTop}>
<Heading type={'section'}>
<I18n id={'page.home.pref.ignored.title'} />
</Heading>
<Paragraph size={'subtitle'}>
<I18n id={'page.home.pref.ignored.description'} />
</Paragraph>
<FactorySelect
id={'ignoredFactoriesSelect'}
factories={ignoredFactories}
onSetFactories={setIgnoredFactories}
/>
</Collapsible>
</Section>
)
}

View File

@@ -0,0 +1,30 @@
.shareGrid {
display: grid;
gap: 3em;
grid-template-columns: repeat(auto-fit, minmax(30ch, 1fr));
}
.downloadBtn {
width: 100%;
}
.uploadInput {
display: inline-block;
width: 100%;
padding: 2em 0.5em;
border: 1px dashed var(--md-sys-color-on-secondary-container);
margin: 1em 0;
background-color: var(--md-sys-color-secondary-container);
border-radius: 4px;
color: var(--md-sys-color-on-secondary-container);
text-align: center;
}
.shareInput {
width: 100%;
}
.uploadInput > input {
width: 0;
visibility: hidden;
}

View File

@@ -0,0 +1,80 @@
import { FC, useEffect, useState } from 'react'
import styles from './SectionShare.module.css'
import { Heading } from '../../shared/Heading'
import { Paragraph } from '../../shared/Paragraph/Paragraph'
import { Button } from '../../shared/Button/Button'
import cx from 'classnames'
import typography from '../../../styles/typography.module.css'
import { download, streamToArrayBuffer } from '../../../src/download'
import { I18n } from '../../shared/I18n/I18n'
import { Input } from '../../shared/Input/Input'
import { Section } from '../../shared/Section/Section'
import { useGroups } from '../../contexts/GroupProvider'
export const SectionShare: FC = () => {
const { store, load } = useGroups()
const [location, setLocation] = useState('')
useEffect(() => {
setLocation(window.location.href)
}, [])
return (
<Section color={'primary'}>
<div className={styles.shareGrid}>
<div>
<Heading type={'section'}>
<I18n id={'page.home.share.download.title'} />
</Heading>
<Paragraph size={'medium'}>
<I18n id={'page.home.share.download.description'} />
</Paragraph>
<Button
className={cx(styles.downloadBtn, typography.bodyLarge)}
onClick={() => {
download('factorio-microservices.bin', store())
}}
>
<I18n id={'page.home.pref.download'} />
</Button>
<label className={styles.uploadInput}>
<I18n id={'page.home.share.download.upload_text'} />
<input
type={'file'}
multiple={false}
onChange={async evt => {
const stream = evt.currentTarget.files?.[0].stream() as
| globalThis.ReadableStream<Uint8Array>
| undefined
if (stream) {
const array = await streamToArrayBuffer(stream)
load(array)
evt.currentTarget.value = null as unknown as string
}
}}
/>
</label>
</div>
<div>
<Heading type={'section'}>
<I18n id={'page.home.share.link.title'} />
</Heading>
<Paragraph size={'medium'}>
<I18n id={'page.home.share.link.description'} />
</Paragraph>
<Paragraph size={'medium'}>
<strong>
<I18n id={'page.home.share.link.warning'} />
</strong>
</Paragraph>
<Input
className={styles.shareInput}
readOnly={true}
value={location}
onClick={e => e.currentTarget.select()}
/>
</div>
</div>
</Section>
)
}

View File

@@ -1,11 +1,7 @@
.icon {
height: 1em;
fill: currentcolor;
padding-inline: 0.5ch;
stroke: currentcolor;
transform: translateY(0.1em);
}
@media (prefers-color-scheme: dark) {
.icon {
filter: invert(100%);
}
}

View File

@@ -0,0 +1,13 @@
.root {
padding: 1em;
border: 0;
background-color: var(--md-sys-color-secondary);
border-radius: 4px;
color: var(--md-sys-color-on-secondary);
transition: box-shadow 0.075s linear;
}
.root:hover,
.root:focus-visible {
box-shadow: 2px 2px 5px 1px rgba(0 0 0 / 50%);
}

View File

@@ -0,0 +1,13 @@
import { ButtonHTMLAttributes, FC } from 'react'
import cx from 'classnames'
import styles from './Button.module.css'
type Props = ButtonHTMLAttributes<HTMLButtonElement>
export const Button: FC<Props> = ({ className, children, ...rest }) => {
return (
<button className={cx(styles.root, className)} {...rest}>
{children}
</button>
)
}

View File

@@ -0,0 +1,15 @@
.root {
display: inline-block;
margin-block-start: -0.4em;
padding-inline: 0.5em;
vertical-align: middle;
}
.link {
display: inline;
padding: 0.2em;
border-radius: 4px;
box-shadow: 0 0 0 1px var(--md-sys-color-tertiary);
color: var(--md-sys-color-tertiary);
font-size: var(--md-sys-typescale-body-small-font-size);
}

View File

@@ -0,0 +1,25 @@
import { FC } from 'react'
import { I18n } from '../I18n/I18n'
import { GraphIcon } from '../../icons/GraphIcon'
import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from './ButtonVisualize.module.css'
interface Props {
groupId?: string
large?: true
}
export const ButtonVisualize: FC<Props> = ({ groupId, large }) => {
const { query } = useRouter()
return (
<span className={styles.root}>
<Link href={{ pathname: !groupId ? '/visualize' : `/visualize/${groupId}`, query }}>
<a className={styles.link}>
{large ? <I18n id={'page.home.pref.visualize'} /> : null}
<GraphIcon />
</a>
</Link>
</span>
)
}

View File

@@ -0,0 +1,58 @@
.collapsible {
position: relative;
}
.toggle {
display: none;
}
.label {
position: absolute;
z-index: 100;
inset-block-start: 0;
inset-inline-end: 0;
}
.label::before {
display: block;
width: 1em;
height: 1em;
border: 1px solid var(--md-ref-palette-neutral-variant80);
border-radius: 4px;
content: "-";
font-size: 0.5em;
line-height: 1em;
margin-block-start: 0.5em;
text-align: center;
}
.content {
position: relative;
overflow: hidden;
max-height: 100vh;
transition: max-height 0.25s ease-in;
}
.content::after {
position: absolute;
height: 3em;
background: linear-gradient(to bottom, rgba(0 0 0 / 0%) 0%, var(--color-bg) 100%);
content: "";
inset-block-end: 0;
inset-inline: 0;
pointer-events: none;
transition: height 0.25s ease-in-out;
}
.toggle:checked + .label::before {
content: "+";
}
.toggle:not(:checked) + .label + .content::after {
height: 0;
}
.toggle:checked + .label + .content {
max-height: 5em;
transition: max-height 0.25s ease-out;
}

View File

@@ -0,0 +1,25 @@
import { FC, PropsWithChildren } from 'react'
import styles from './Collapsible.module.css'
import cx from 'classnames'
import typography from '../../../styles/typography.module.css'
interface Props {
id: string
className?: string
}
export const Collapsible: FC<PropsWithChildren<Props>> = ({ id, className, children }) => {
return (
<div className={cx(styles.collapsible, className)}>
<input id={id} className={styles.toggle} type='checkbox' defaultChecked={true} />
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
htmlFor={id}
id={id}
className={cx(styles.label, typography.displaySmall)}
defaultChecked={true}
/>
<div className={styles.content}>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,35 @@
import { createElement, FC, HTMLAttributes, PropsWithChildren } from 'react'
import cx from 'classnames'
import typography from '../../styles/typography.module.css'
interface Props extends HTMLAttributes<HTMLSpanElement & HTMLHeadingElement> {
tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'span'
type: 'pageTitle' | 'section' | 'subsection'
className?: string
}
export const Heading: FC<PropsWithChildren<Props>> = ({
tag,
type,
className,
children,
...rest
}) => {
const TAG: Record<Props['type'], Exclude<Props['tag'], undefined>> = {
pageTitle: 'h1',
section: 'h2',
subsection: 'h3'
}
return createElement(
tag ?? TAG[type],
{
className: cx(className, {
[typography.displayLarge]: type === 'pageTitle',
[typography.headlineMedium]: type === 'section',
[typography.titleLarge]: type === 'subsection'
}),
...rest
},
children
)
}

View File

@@ -0,0 +1,8 @@
.root {
padding: 1em 0.5em;
border: 1px solid var(--color-text, var(--md-sys-color-on-background));
margin: 0 0 1em;
background-color: var(--color-bg, var(--md-sys-color-background));
border-radius: 4px;
font-size: var(--md-sys-typescale-body-small-font-size);
}

View File

@@ -0,0 +1,9 @@
import { FC, InputHTMLAttributes } from 'react'
import styles from './Input.module.css'
import cx from 'classnames'
type Props = InputHTMLAttributes<HTMLInputElement>
export const Input: FC<Props> = ({ className, ...rest }) => {
return <input className={cx(className, styles.root)} {...rest} />
}

View File

@@ -0,0 +1,3 @@
.subtitle {
margin-block-start: -2em;
}

View File

@@ -0,0 +1,23 @@
import { FC, PropsWithChildren } from 'react'
import cx from 'classnames'
import typography from '../../../styles/typography.module.css'
import styles from './Paragraph.module.css'
interface Props {
size: 'large' | 'medium' | 'small' | 'subtitle'
}
export const Paragraph: FC<PropsWithChildren<Props>> = ({ size, children }) => {
return (
<p
className={cx({
[typography.bodyLarge]: size === 'large',
[typography.bodyMedium]: size === 'medium',
[typography.bodySmall]: size === 'small' || size === 'subtitle',
[styles.subtitle]: size === 'subtitle'
})}
>
{children}
</p>
)
}

View File

@@ -0,0 +1,33 @@
.content {
max-width: 80ch;
margin: 0 auto;
padding-block: 2em;
text-align: justify;
--color-bg: var(--md-sys-color-background);
--color-text: var(--md-sys-color-on-background);
}
.primary {
--color-bg: var(--md-sys-color-primary-container);
--color-text: var(--md-sys-color-on-primary-container);
}
.secondary {
--color-bg: var(--md-sys-color-secondary-container);
--color-text: var(--md-sys-color-on-secondary-container);
}
.tertiary {
--color-bg: var(--md-sys-color-tertiary-container);
--color-text: var(--md-sys-color-on-tertiary-container);
}
.primary,
.secondary,
.tertiary {
background-color: var(--color-bg);
box-shadow: 0 0 0 100vmax var(--color-bg);
clip-path: inset(0 -100vmax);
color: var(--color-text);
}

View File

@@ -0,0 +1,26 @@
import { createContext, FC, PropsWithChildren, useContext } from 'react'
import styles from './Section.module.css'
import cx from 'classnames'
type SectionContextType = 'primary' | 'secondary' | 'tertiary' | undefined
interface Props {
color?: SectionContextType
}
const SectionContext = createContext<SectionContextType>(undefined)
export const useSectionColor = () => useContext(SectionContext)
export const Section: FC<PropsWithChildren<Props>> = ({ color, children }) => {
return (
<div
className={cx(styles.content, {
[styles.primary]: color === 'primary',
[styles.secondary]: color === 'secondary',
[styles.tertiary]: color === 'tertiary'
})}
>
<SectionContext.Provider value={color}>{children}</SectionContext.Provider>
</div>
)
}

View File

@@ -43,7 +43,7 @@ export const NodeOverview: FC<Props> = ({ node, className, ...props }) => {
</div>
) : null}
<h4>
<I18n id={'page.visualize.imports'} />
<I18n id={'page.visualize.overview.imports'} />
</h4>
<div className={styles.small}>
{node.inputs.map(input => (
@@ -53,7 +53,7 @@ export const NodeOverview: FC<Props> = ({ node, className, ...props }) => {
{node.outputs.length ? (
<>
<h4>
<I18n id={'page.visualize.exports'} />
<I18n id={'page.visualize.overview.exports'} />
</h4>
<div className={styles.small}>
{node.outputs.map(input => (

View File

@@ -9,8 +9,11 @@ import Head from 'next/head'
import { ScrollContainer } from './ScrollContainer/ScrollContainer'
import { ProducingGraph } from './ProducingGraph/ProducingGraph'
import { useFactories } from '../contexts/FactoryProvider'
import { i18n, I18n } from '../shared/I18n/I18n'
import { useIntl } from 'react-intl'
export const PageDetails: FC = () => {
const intl = useIntl()
const {
query: { name }
} = useRouter()
@@ -61,12 +64,16 @@ export const PageDetails: FC = () => {
return (
<>
<Head>
<title>Factorio Microservices</title>
<meta name='description' content='Create Factorio microservices' />
<title>
<I18n id={'page.visualize.details.title'} values={{ name: group?.name ?? '' }} />
</title>
<meta name='description' content={i18n(intl, 'page.home.head.meta.description')} />
</Head>
<main>
<ScrollContainer>
<h1>{name}</h1>
<h1>
<I18n id={'page.visualize.details.title'} values={{ name: group?.name ?? '' }} />
</h1>
<ProducingGraph
nodes={producingNodes}
inputs={inputFactories}

View File

@@ -6,8 +6,11 @@ import { ScrollContainer } from './ScrollContainer/ScrollContainer'
import { ProducingGraph } from './ProducingGraph/ProducingGraph'
import { NodeOverview, OverviewGraphNode } from './NodeOverview/NodeOverview'
import { useFactories } from '../contexts/FactoryProvider'
import { i18n, I18n } from '../shared/I18n/I18n'
import { useIntl } from 'react-intl'
export const PageOverview: FC = () => {
const intl = useIntl()
const { exportedFactories, baseFactories, groups } = useGroups()
const { findFactory } = useFactories()
@@ -28,12 +31,16 @@ export const PageOverview: FC = () => {
return (
<>
<Head>
<title>Factorio Microservices</title>
<meta name='description' content='Create Factorio microservices' />
<title>
<I18n id={'page.visualize.details.title'} />
</title>
<meta name='description' content={i18n(intl, 'page.home.head.meta.description')} />
</Head>
<main>
<ScrollContainer>
<h1>Factorio Microservices</h1>
<h1>
<I18n id={'page.visualize.details.title'} />
</h1>
<ProducingGraph
nodes={producingNodes}
inputs={baseFactories}