Styling of home
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.addBtn {
|
||||
margin-inline: 1em;
|
||||
}
|
||||
|
||||
.entitySpanList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.1em;
|
||||
}
|
||||
123
components/home/SectionAddMissing/SectionAddMissing.tsx
Normal file
123
components/home/SectionAddMissing/SectionAddMissing.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.margin-top {
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
45
components/home/SectionPreferences/SectionPreferences.tsx
Normal file
45
components/home/SectionPreferences/SectionPreferences.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
30
components/home/SectionShare/SectionShare.module.css
Normal file
30
components/home/SectionShare/SectionShare.module.css
Normal 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;
|
||||
}
|
||||
80
components/home/SectionShare/SectionShare.tsx
Normal file
80
components/home/SectionShare/SectionShare.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
13
components/shared/Button/Button.module.css
Normal file
13
components/shared/Button/Button.module.css
Normal 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%);
|
||||
}
|
||||
13
components/shared/Button/Button.tsx
Normal file
13
components/shared/Button/Button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
15
components/shared/ButtonVisualize/ButtonVisualize.module.css
Normal file
15
components/shared/ButtonVisualize/ButtonVisualize.module.css
Normal 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);
|
||||
}
|
||||
25
components/shared/ButtonVisualize/ButtonVisualize.tsx
Normal file
25
components/shared/ButtonVisualize/ButtonVisualize.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
58
components/shared/Collapsible/Collapsible.module.css
Normal file
58
components/shared/Collapsible/Collapsible.module.css
Normal 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;
|
||||
}
|
||||
25
components/shared/Collapsible/Collapsible.tsx
Normal file
25
components/shared/Collapsible/Collapsible.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
35
components/shared/Heading.tsx
Normal file
35
components/shared/Heading.tsx
Normal 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
|
||||
)
|
||||
}
|
||||
8
components/shared/Input/Input.module.css
Normal file
8
components/shared/Input/Input.module.css
Normal 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);
|
||||
}
|
||||
9
components/shared/Input/Input.tsx
Normal file
9
components/shared/Input/Input.tsx
Normal 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} />
|
||||
}
|
||||
3
components/shared/Paragraph/Paragraph.module.css
Normal file
3
components/shared/Paragraph/Paragraph.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.subtitle {
|
||||
margin-block-start: -2em;
|
||||
}
|
||||
23
components/shared/Paragraph/Paragraph.tsx
Normal file
23
components/shared/Paragraph/Paragraph.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
33
components/shared/Section/Section.module.css
Normal file
33
components/shared/Section/Section.module.css
Normal 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);
|
||||
}
|
||||
26
components/shared/Section/Section.tsx
Normal file
26
components/shared/Section/Section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user