code:
import { useBackend, useSharedState } from '../backend';
import { AnimatedNumber, Box, Button, Flex, LabeledList, ProgressBar, Section, Slider, Tabs } from '../components';
import { NtosWindow } from '../layouts';

export const NtosRobotact = (props, context) => {
  const { act, data } = useBackend(context);
  const { PC_device_theme } = data;
  return (
    <NtosWindow
      width={800}
      height={600}
      theme={PC_device_theme}>
      <NtosWindow.Content>
        <NtosRobotactContent />
      </NtosWindow.Content>
    </NtosWindow>
  );
};

export const NtosRobotactContent = (props, context) => {
  const { act, data } = useBackend(context);
  const [tab_main, setTab_main] = useSharedState(context, 'tab_main', 1);
  const [tab_sub, setTab_sub] = useSharedState(context, 'tab_sub', 1);
  const {
    charge,
    maxcharge,
    integrity,
    lampIntensity,
    cover,
    locomotion,
    wireModule,
    wireCamera,
    wireAI,
    wireLaw,
    sensors,
    printerPictures,
    printerToner,
    printerTonerMax,
    thrustersInstalled,
    thrustersStatus,
    selfDestructAble,
  } = data;
  const borgName = data.name || [];
  const borgType = data.designation || [];
  const masterAI = data.masterAI || [];
  const laws = data.Laws || [];
  const borgLog = data.borgLog || [];
  const borgUpgrades = data.borgUpgrades || [];
  return (
    <Flex
      direction={"column"}>
      <Flex.Item
        position="relative"
        mb={1}>
        <Tabs>
          <Tabs.Tab
            icon="list"
            lineHeight="23px"
            selected={tab_main === 1}
            onClick={() => setTab_main(1)}>
            Status
          </Tabs.Tab>
          <Tabs.Tab
            icon="list"
            lineHeight="23px"
            selected={tab_main === 2}
            onClick={() => setTab_main(2)}>
            Logs
          </Tabs.Tab>
        </Tabs>
      </Flex.Item>
      {tab_main === 1 && (
        <>
          <Flex
            direction={"row"}>
            <Flex.Item
              width="30%">

                <LabeledList>
                  <LabeledList.Item
                    label="Unit">
                    {borgName.slice(0, 17)}
                  </LabeledList.Item>
                  <LabeledList.Item
                    label="Type">
                    {borgType}
                  </LabeledList.Item>
                  <LabeledList.Item
                    label="AI">
                    {masterAI.slice(0, 17)}
                  </LabeledList.Item>
                </LabeledList>

            </Flex.Item>
            <Flex.Item
              grow={1}
              basis="content"
              ml={1}>

                Charge:
                <Button
                  content="Power Alert"
                  disabled={charge}
                  onClick={() => act('alertPower')} />
                <ProgressBar
                  value={charge / maxcharge}
                  ranges={{
                    good: [0.5, Infinity],
                    average: [0.1, 0.5],
                    bad: [-Infinity, 0.1],
                  }}>
                  <AnimatedNumber value={charge} />
                </ProgressBar>
                Chassis Integrity:
                <ProgressBar
                  value={integrity}
                  minValue={0}
                  maxValue={100}
                  ranges={{
                    bad: [-Infinity, 25],
                    average: [25, 75],
                    good: [75, Infinity],
                  }} />

                <Slider
                  value={lampIntensity}
                  step={1}
                  stepPixelSize={25}
                  maxValue={5}
                  minValue={1}
                  onChange={(e, value) => act('lampIntensity', {
                    ref: value,
                  })} />
                Lamp power usage: {lampIntensity/2} watts

            </Flex.Item>
            <Flex.Item
              width="50%"
              ml={1}>

                <Tabs
                  fluid={1}
                  textAlign="center">
                  <Tabs.Tab
                    icon=""
                    lineHeight="23px"
                    selected={tab_sub === 1}
                    onClick={() => setTab_sub(1)}>
                    Actions
                  </Tabs.Tab>
                  <Tabs.Tab
                    icon=""
                    lineHeight="23px"
                    selected={tab_sub === 2}
                    onClick={() => setTab_sub(2)}>
                    Upgrades
                  </Tabs.Tab>
                  <Tabs.Tab
                    icon=""
                    lineHeight="23px"
                    selected={tab_sub === 3}
                    onClick={() => setTab_sub(3)}>
                    Diagnostics
                  </Tabs.Tab>
                </Tabs>

              {tab_sub === 1 && (

                  <LabeledList>
                    <LabeledList.Item
                      label="Maintenance Cover">
                      <Button.Confirm
                        content="Unlock"
                        disabled={cover==="UNLOCKED"}
                        onClick={() => act('coverunlock')} />
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="Sensor Overlay">
                      <Button
                        content={sensors}
                        onClick={() => act('toggleSensors')} />
                    </LabeledList.Item>
                    <LabeledList.Item
                      label={"Stored Photos (" + printerPictures + ")"}>
                      <Button
                        content="View"
                        disabled={!printerPictures}
                        onClick={() => act('viewImage')} />
                      <Button
                        content="Print"
                        disabled={!printerPictures}
                        onClick={() => act('printImage')} />
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="Printer Toner">
                      <ProgressBar
                        value={printerToner / printerTonerMax} />
                    </LabeledList.Item>
                    {!!thrustersInstalled && (
                      <LabeledList.Item
                        label="Toggle Thrusters">
                        <Button
                          content={thrustersStatus}
                          onClick={() => act('toggleThrusters')} />
                      </LabeledList.Item>
                    )}
                    {!!selfDestructAble && (
                      <LabeledList.Item
                        label="Self Destruct">
                        <Button.Confirm
                          content="ACTIVATE"
                          color="red"
                          onClick={() => act('selfDestruct')} />
                      </LabeledList.Item>
                    )}
                  </LabeledList>

              )}
              {tab_sub === 2 && (

                  {borgUpgrades.map(upgrade => (
                    <Box
                      mb={1}
                      key={upgrade}>
                      {upgrade}
                    </Box>
                  ))}

              )}
              {tab_sub === 3 && (

                  <LabeledList>
                    <LabeledList.Item
                      label="AI Connection"
                      color={wireAI==="FAULT"?'red': wireAI==="READY"?'yellow': 'green'}>
                      {wireAI}
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="LawSync"
                      color={wireLaw==="FAULT"?"red":"green"}>
                      {wireLaw}
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="Camera"
                      color={wireCamera==="FAULT"?'red': wireCamera==="DISABLED"?'yellow': 'green'}>
                      {wireCamera}
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="Module Controller"
                      color={wireModule==="FAULT"?"red":"green"}>
                      {wireModule}
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="Motor Controller"
                      color={locomotion==="FAULT"?'red': locomotion==="DISABLED"?'yellow': 'green'}>
                      {locomotion}
                    </LabeledList.Item>
                    <LabeledList.Item
                      label="Maintenance Cover"
                      color={cover==="UNLOCKED"?"red":"green"}>
                      {cover}
                    </LabeledList.Item>
                  </LabeledList>

              )}
            </Flex.Item>
          </Flex>
          <Flex.Item
            height={21}
            mt={1}>
            <Section
              title="Laws"
              fill
              scrollable
              buttons={(
                <>
                  <Button
                    content="State Laws"
                    onClick={() => act('lawstate')} />
                  <Button
                    icon="volume-off"
                    onClick={() => act('lawchannel')} />
                </>
              )}>
              {laws.map(law => (
                <Box
                  mb={1}
                  key={law}>
                  {law}
                </Box>
              ))}

          </Flex.Item>
        </>
      )}
      {tab_main === 2 && (
        <Flex.Item
          height={40}>

            {borgLog.map(log => (
              <Box
                mb={1}
                key={log}>
                {log}
              </Box>
            ))}

        </Flex.Item>
      )}
    </Flex>
  );
};


code:
import { useBackend } from '../backend';
import { Button, ByondUi } from '../components';
import { NtosWindow } from '../layouts';
import { prevNextCamera, selectCameras, CameraConsoleContent } from './CameraConsole';

export const NtosSecurEye = (props, context) => {
  const { act, data, config } = useBackend(context);
  const { PC_device_theme, mapRef, activeCamera } = data;
  const cameras = selectCameras(data.cameras);
  const [
    prevCameraName,
    nextCameraName,
  ] = prevNextCamera(cameras, activeCamera);
  return (
    <NtosWindow
      width={800}
      height={600}
      theme={PC_device_theme}>
      <NtosWindow.Content>

          <CameraConsoleContent />

            Camera:
            {activeCamera
              && activeCamera.name
              || '—'}

            <Button
              icon="chevron-left"
              disabled={!prevCameraName}
              onClick={() => act('switch_camera', {
                name: prevCameraName,
              })} />
            <Button
              icon="chevron-right"
              disabled={!nextCameraName}
              onClick={() => act('switch_camera', {
                name: nextCameraName,
              })} />

          <ByondUi
            className="CameraConsole__map"
            params={{
              id: mapRef,
              type: 'map',
            }} />

      </NtosWindow.Content>
    </NtosWindow>
  );
};

repo_name: AnalWerewolf/sundown

license: mit


code:
import { useBackend } from '../backend';
import { Box, Button, LabeledList, Section } from '../components';
import { NtosWindow } from '../layouts';

export const NtosShipping = (props, context) => {
  const { act, data } = useBackend(context);
  return (
    <NtosWindow
      width={450}
      height={350}>
      <NtosWindow.Content scrollable>
        <Section
          title="NTOS Shipping Hub."
          buttons={(
            <Button
              icon="eject"
              content="Eject Id"
              onClick={() => act('ejectid')} />
          )}>
          <LabeledList>
            <LabeledList.Item label="Current User">
              {data.current_user || "N/A"}
            </LabeledList.Item>
            <LabeledList.Item label="Inserted Card">
              {data.card_owner || "N/A"}
            </LabeledList.Item>
            <LabeledList.Item label="Available Paper">
              {data.has_printer ? data.paperamt : "N/A"}
            </LabeledList.Item>
            <LabeledList.Item label="Profit on Sale">
              {data.barcode_split}%
            </LabeledList.Item>
          </LabeledList>
        </Section>
        <Section title="Shipping Options">
          <Box>
            <Button
              icon="id-card"
              tooltip="The currently ID card will become the current user."
              tooltipPosition="right"
              disabled={!data.has_id_slot}
              onClick={() => act('selectid')}
              content="Set Current ID" />
          </Box>
          <Box>
            <Button
              icon="print"
              tooltip="Print a barcode to use on a wrapped package."
              tooltipPosition="right"
              disabled={!data.has_printer || !data.current_user}
              onClick={() => act('print')}
              content="Print Barcode" />
          </Box>
          <Box>
            <Button
              icon="tags"
              tooltip="Set how much profit you'd like on your package."
              tooltipPosition="right"
              onClick={() => act('setsplit')}
              content="Set Profit Margin" />
          </Box>
          <Box>
            <Button
              icon="sync-alt"
              content="Reset ID"
              onClick={() => act('resetid')} />
          </Box>
        </Section>
      </NtosWindow.Content>
    </NtosWindow>
  );
};


code:
import { SignalerContent } from './Signaler';
import { NtosWindow } from '../layouts';

export const NtosSignaler = (props, context) => {
  return (
    <NtosWindow
      width={400}
      height={300}>
      <NtosWindow.Content>
        <SignalerContent />
      </NtosWindow.Content>
    </NtosWindow>
  );
};


code:
import { NtosWindow } from '../layouts';
import { StationAlertConsoleContent } from './StationAlertConsole';

export const NtosStationAlertConsole = () => {
  return (
    <NtosWindow
      width={335}
      height={587}>
      <NtosWindow.Content scrollable>
        <StationAlertConsoleContent />
      </NtosWindow.Content>
    </NtosWindow>
  );
};


code:
import { NtosWindow } from '../layouts';
import { SupermatterMonitorContent } from './SupermatterMonitor';

export const NtosSupermatterMonitor = (props, context) => {
  return (
    <NtosWindow
      width={600}
      height={350}>
      <NtosWindow.Content scrollable>
        <SupermatterMonitorContent />
      </NtosWindow.Content>
    </NtosWindow>
  );
};


code:
import { AppTechweb } from './Techweb.js';
import { useBackend } from '../backend';
import { createLogger } from '../logging';

const logger = createLogger('backend');

export const NtosTechweb = (props, context) => {
  const { config, data, act } = useBackend(context);
  logger.log(config.AppTechweb);
  return (
    <AppTechweb />
  );
};


code:
import { classes } from 'common/react';
import { useBackend } from '../backend';
import { Box, Button, Flex, Grid, Icon } from '../components';
import { Window } from '../layouts';

// This ui is so many manual overrides and !important tags
// and hand made width sets that changing pretty much anything
// is going to require a lot of tweaking it get it looking correct again
// I'm sorry, but it looks bangin
const NukeKeypad = (props, context) => {
  const { act } = useBackend(context);
  const keypadKeys = [
    ['1', '4', '7', 'C'],
    ['2', '5', '8', '0'],
    ['3', '6', '9', 'E'],
  ];
  return (
    <Box width="185px">
      <Grid width="1px">
        {keypadKeys.map(keyColumn => (
          <Grid.Column key={keyColumn[0]}>
            {keyColumn.map(key => (
              <Button
                fluid
                bold
                key={key}
                mb="6px"
                content={key}
                textAlign="center"
                fontSize="40px"
                lineHeight={1.25}
                width="55px"
                className={classes([
                  'NuclearBomb__Button',
                  'NuclearBomb__Button--keypad',
                  'NuclearBomb__Button--' + key,
                ])}
                onClick={() => act('keypad', { digit: key })} />
            ))}
          </Grid.Column>
        ))}
      </Grid>
    </Box>
  );
};

export const NuclearBomb = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    anchored,
    disk_present,
    status1,
    status2,
  } = data;
  return (
    <Window
      width={350}
      height={442}
      theme="retro">
      <Window.Content>
        <Box m="6px">
          <Box
            mb="6px"
            className="NuclearBomb__displayBox">
            {status1}
          </Box>
          <Flex mb={1.5}>
            <Flex.Item grow={1}>
              <Box className="NuclearBomb__displayBox">
                {status2}
              </Box>
            </Flex.Item>
            <Flex.Item>
              <Button
                icon="eject"
                fontSize="24px"
                lineHeight={1}
                textAlign="center"
                width="43px"
                ml="6px"
                mr="3px"
                mt="3px"
                className="NuclearBomb__Button NuclearBomb__Button--keypad"
                onClick={() => act('eject_disk')} />
            </Flex.Item>
          </Flex>
          <Flex ml="3px">
            <Flex.Item>
              <NukeKeypad />
            </Flex.Item>
            <Flex.Item ml="6px" width="129px">
              <Box>
                <Button
                  fluid
                  bold
                  content="ARM"
                  textAlign="center"
                  fontSize="28px"
                  lineHeight={1.1}
                  mb="6px"
                  className="NuclearBomb__Button NuclearBomb__Button--C"
                  onClick={() => act('arm')} />
                <Button
                  fluid
                  bold
                  content="ANCHOR"
                  textAlign="center"
                  fontSize="28px"
                  lineHeight={1.1}
                  className="NuclearBomb__Button NuclearBomb__Button--E"
                  onClick={() => act('anchor')} />
                <Box
                  textAlign="center"
                  color="#9C9987"
                  fontSize="80px">
                  <Icon name="radiation" />
                </Box>
                <Box
                  height="80px"
                  className="NuclearBomb__NTIcon" />
              </Box>
            </Flex.Item>
          </Flex>
        </Box>
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend, useSharedState } from '../backend';
import { AnimatedNumber, Button, LabeledList, NoticeBox, ProgressBar, Section, Tabs } from '../components';
import { Window } from '../layouts';

const damageTypes = [
  {
    label: 'Brute',
    type: 'bruteLoss',
  },
  {
    label: 'Burn',
    type: 'fireLoss',
  },
  {
    label: 'Toxin',
    type: 'toxLoss',
  },
  {
    label: 'Respiratory',
    type: 'oxyLoss',
  },
];

export const OperatingComputer = (props, context) => {
  const [tab, setTab] = useSharedState(context, 'tab', 1);
  return (
    <Window
      width={350}
      height={470}>
      <Window.Content scrollable>
        <Tabs>
          <Tabs.Tab
            selected={tab === 1}
            onClick={() => setTab(1)}>
            Patient State
          </Tabs.Tab>
          <Tabs.Tab
            selected={tab === 2}
            onClick={() => setTab(2)}>
            Surgery Procedures
          </Tabs.Tab>
        </Tabs>
        {tab === 1 && (
          <PatientStateView />
        )}
        {tab === 2 && (
          <SurgeryProceduresView />
        )}
      </Window.Content>
    </Window>
  );
};

const PatientStateView = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    table,
    procedures = [],
    patient = {},
  } = data;
  if (!table) {
    return (
      <NoticeBox>
        No Table Detected
      </NoticeBox>
    );
  }
  return (
    <>

        {patient && (
          <LabeledList>
            <LabeledList.Item
              label="State"
              color={patient.statstate}>
              {patient.stat}
            </LabeledList.Item>
            <LabeledList.Item label="Blood Type">
              {patient.blood_type}
            </LabeledList.Item>
            <LabeledList.Item label="Health">
              <ProgressBar
                value={patient.health}
                minValue={patient.minHealth}
                maxValue={patient.maxHealth}
                color={patient.health >= 0 ? 'good' : 'average'}>
                <AnimatedNumber value={patient.health} />
              </ProgressBar>
            </LabeledList.Item>
            {damageTypes.map(type => (
              <LabeledList.Item key={type.type} label={type.label}>
                <ProgressBar
                  value={patient[type.type] / patient.maxHealth}
                  color="bad">
                  <AnimatedNumber value={patient[type.type]} />
                </ProgressBar>
              </LabeledList.Item>
            ))}
          </LabeledList>
        ) || (
          'No Patient Detected'
        )}

      {procedures.length === 0 && (

          No Active Procedures

      )}
      {procedures.map(procedure => (

          <LabeledList>
            <LabeledList.Item label="Next Step">
              {procedure.next_step}
              {procedure.chems_needed && (
                <>
                  Required Chemicals:

                  {procedure.chems_needed}
                </>
              )}
            </LabeledList.Item>
            {!!data.alternative_step && (
              <LabeledList.Item label="Alternative Step">
                {procedure.alternative_step}
                {procedure.alt_chems_needed && (
                  <>
                    Required Chemicals:

                    {procedure.alt_chems_needed}
                  </>
                )}
              </LabeledList.Item>
            )}
          </LabeledList>

      ))}
    </>
  );
};

const SurgeryProceduresView = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    surgeries = [],
  } = data;
  return (

      <Button
        icon="download"
        content="Sync Research Database"
        onClick={() => act('sync')} />
      {surgeries.map(surgery => (

          {surgery.desc}

      ))}

  );
};


code:
import { createSearch } from 'common/string';
import { multiline } from 'common/string';
import { resolveAsset } from '../assets';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Divider, Flex, Icon, Input, Section } from '../components';
import { Window } from '../layouts';

const PATTERN_NUMBER = / \(([0-9]+)\)$/;

const searchFor = searchText => createSearch(searchText, thing => thing.name);

const compareString = (a, b) => a  b;

const compareNumberedText = (a, b) => {
  const aName = a.name;
  const bName = b.name;

  // Check if aName and bName are the same except for a number at the end
  // e.g. Medibot (2) and Medibot (3)
  const aNumberMatch = aName.match(PATTERN_NUMBER);
  const bNumberMatch = bName.match(PATTERN_NUMBER);

  if (aNumberMatch
    && bNumberMatch
    && aName.replace(PATTERN_NUMBER, "") === bName.replace(PATTERN_NUMBER, "")
  ) {
    const aNumber = parseInt(aNumberMatch[1], 10);
    const bNumber = parseInt(bNumberMatch[1], 10);

    return aNumber - bNumber;
  }

  return compareString(aName, bName);
};

const BasicSection = (props, context) => {
  const { act } = useBackend(context);
  const { searchText, source, title, autoObserve } = props;

  const things = source.filter(searchFor(searchText));
  things.sort(compareNumberedText);
  return source.length > 0 && (
    <Section title={`${title} - (${source.length})`}>
      {things.map(thing => (
        <Button
          key={thing.name}
          content={thing.name}
          onClick={() => act("orbit", {
            ref: thing.ref,
            auto_observe: autoObserve,
          })} />
      ))}

  );
};

const OrbitedButton = (props, context) => {
  const { act } = useBackend(context);
  const { color, thing, autoObserve } = props;

  return (
    <Button
      color={color}
      onClick={() => act("orbit", {
        ref: thing.ref,
        auto_observe: autoObserve,
      })}>
      {thing.name}
      {thing.orbiters && (
        <Box inline ml={1}>
          {"("}{thing.orbiters}{" "}
          <Box
            as="img"
            src={resolveAsset('ghost.png')}
            opacity={0.7} />
          {")"}
        </Box>
      )}
    </Button>
  );
};

export const Orbit = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    alive,
    antagonists,
    dead,
    ghosts,
    misc,
    npcs,
  } = data;

  const [searchText, setSearchText] = useLocalState(context, "searchText", "");
  const [autoObserve, setAutoObserve] = useLocalState(context, "autoObserve", false);

  const collatedAntagonists = {};
  for (const antagonist of antagonists) {
    if (collatedAntagonists[antagonist.antag] === undefined) {
      collatedAntagonists[antagonist.antag] = [];
    }
    collatedAntagonists[antagonist.antag].push(antagonist);
  }

  const sortedAntagonists = Object.entries(collatedAntagonists);
  sortedAntagonists.sort((a, b) => {
    return compareString(a[0], b[0]);
  });

  const orbitMostRelevant = searchText => {
    for (const source of [
      sortedAntagonists.map(([_, antags]) => antags),
      alive, ghosts, dead, npcs, misc,
    ]) {
      const member = source
        .filter(searchFor(searchText))
        .sort(compareNumberedText)[0];
      if (member !== undefined) {
        act("orbit", {
          ref: member.ref,
          auto_observe: autoObserve,
        });
        break;
      }
    }
  };

  return (
    <Window
      title="Orbit"
      width={350}
      height={700}>
      <Window.Content scrollable>

          <Flex>
            <Flex.Item>
              <Icon
                name="search"
                mr={1} />
            </Flex.Item>
            <Flex.Item grow={1}>
              <Input
                placeholder="Search..."
                autoFocus
                fluid
                value={searchText}
                onInput={(_, value) => setSearchText(value)}
                onEnter={(_, value) => orbitMostRelevant(value)} />
            </Flex.Item>
            <Flex.Item>
              <Divider vertical />
            </Flex.Item>
            <Flex.Item>
              <Button
                inline
                color="transparent"
                tooltip={multiline`Toggle Auto-Observe. When active, you'll
                see the UI / full inventory of whoever you're orbiting. Neat!`}
                tooltipPosition="bottom-start"
                selected={autoObserve}
                icon={autoObserve ? "toggle-on" : "toggle-off"}
                onClick={() => setAutoObserve(!autoObserve)} />
            </Flex.Item>
          </Flex>

        {antagonists.length > 0 && (

            {sortedAntagonists.map(([name, antags]) => (

                {antags
                  .filter(searchFor(searchText))
                  .sort(compareNumberedText)
                  .map(antag => (
                    <OrbitedButton
                      key={antag.name}
                      color="bad"
                      thing={antag}
                      autoObserve={autoObserve}
                    />
                  ))}

            ))}

        )}

        <Section title={`Alive - (${alive.length})`}>
          {alive
            .filter(searchFor(searchText))
            .sort(compareNumberedText)
            .map(thing => (
              <OrbitedButton
                key={thing.name}
                color="good"
                thing={thing}
                autoObserve={autoObserve} />
            ))}

        <Section title={`Ghosts - (${ghosts.length})`}>
          {ghosts
            .filter(searchFor(searchText))
            .sort(compareNumberedText)
            .map(thing => (
              <OrbitedButton
                key={thing.name}
                color="grey"
                thing={thing}
                autoObserve={autoObserve} />
            ))}

        <BasicSection
          title="Dead"
          source={dead}
          searchText={searchText}
          autoObserve={autoObserve}
        />

        <BasicSection
          title="NPCs"
          source={npcs}
          searchText={searchText}
        />

        <BasicSection
          title="Misc"
          source={misc}
          searchText={searchText}
        />
      </Window.Content>
    </Window>
  );
};


code:
import { toTitleCase } from 'common/string';
import { Box, Button, Section, Table } from '../components';
import { useBackend } from '../backend';
import { Window } from '../layouts';

export const OreBox = (props, context) => {
  const { act, data } = useBackend(context);
  const { materials } = data;
  return (
    <Window
      width={335}
      height={415}>
      <Window.Content scrollable>
        <Section
          title="Ores"
          buttons={(
            <Button
              content="Empty"
              onClick={() => act('removeall')} />
          )}>

                Ore

                Amount

            {materials.map(material => (

                  {toTitleCase(material.name)}

                  <Box color="label" inline>
                    {material.amount}
                  </Box>

            ))}

          <Box>
            All ores will be placed in here when you are wearing a mining
            stachel on your belt or in a pocket while dragging the ore box.

            Gibtonite is not accepted.
          </Box>

      </Window.Content>
    </Window>
  );
};


code:
import { toTitleCase } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { BlockQuote, Box, Button, NumberInput, Section, Table } from '../components';
import { Window } from '../layouts';

export const OreRedemptionMachine = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    unclaimedPoints,
    materials,
    alloys,
    diskDesigns,
    hasDisk,
  } = data;
  return (
    <Window
      title="Ore Redemption Machine"
      width={440}
      height={550}>
      <Window.Content scrollable>

            This machine only accepts ore.

            Gibtonite and Slag are not accepted.

          <Box>
            <Box inline color="label" mr={1}>
              Unclaimed points:
            </Box>
            {unclaimedPoints}
            <Button
              ml={2}
              content="Claim"
              disabled={unclaimedPoints === 0}
              onClick={() => act('Claim')} />
          </Box>

          {hasDisk && (
            <>
              <Box mb={1}>
                <Button
                  icon="eject"
                  content="Eject design disk"
                  onClick={() => act('diskEject')} />
              </Box>

                {diskDesigns.map(design => (

                      File {design.index}: {design.name}

                      <Button
                        disabled={!design.canupload}
                        content="Upload"
                        onClick={() => act('diskUpload', {
                          design: design.index,
                        })} />

                ))}

            </>
          ) || (
            <Button
              icon="save"
              content="Insert design disk"
              onClick={() => act('diskInsert')} />
          )}

            {materials.map(material => (
              <MaterialRow
                key={material.id}
                material={material}
                onRelease={amount => act('Release', {
                  id: material.id,
                  sheets: amount,
                })} />
            ))}

            {alloys.map(material => (
              <MaterialRow
                key={material.id}
                material={material}
                onRelease={amount => act('Smelt', {
                  id: material.id,
                  sheets: amount,
                })} />
            ))}

      </Window.Content>
    </Window>
  );
};

const MaterialRow = (props, context) => {
  const { material, onRelease } = props;

  const [
    amount,
    setAmount,
  ] = useLocalState(context, "amount" + material.name, 1);

  const amountAvailable = Math.floor(material.amount);
  return (

        {toTitleCase(material.name).replace('Alloy', '')}

        <Box mr={2} color="label" inline>
          {material.value && material.value + ' cr'}
        </Box>

        <Box mr={2} color="label" inline>
          {amountAvailable} sheets
        </Box>

        <NumberInput
          width="32px"
          step={1}
          stepPixelSize={5}
          minValue={1}
          maxValue={50}
          value={amount}
          onChange={(e, value) => setAmount(value)} />
        <Button
          disabled={amountAvailable < 1}
          content="Release"
          onClick={() => onRelease(amount)} />

  );
};


code:
import { useBackend } from '../backend';
import { Box, Button, Divider, Section, Stack } from '../components';
import { Window } from '../layouts';

const buttonWidth = 2;

const goodstyle = {
  color: 'lightgreen',
  fontWeight: 'bold',
};

const badstyle = {
  color: 'red',
  fontWeight: 'bold',
};

const partstyle = {
  color: 'yellow',
  fontWeight: 'bold',
};

const fuelstyle = {
  color: 'olive',
  fontWeight: 'bold',
};

const variousButtonIcons = {
  "Restore Hull": "wrench",
  "Fix Engine": "rocket",
  "Repair Electronics": "server",
  "Wait": "clock",
  "Continue": "arrow-right",
  "Explore Ship": "door-open",
  "Leave the Derelict": "arrow-right",
  "Welcome aboard.": "user-plus",
  "Where did you go?!": "user-minus",
  "A good find.": "box-open",
  "Continue travels.": "arrow-right",
  "Keep Speed": "tachometer-alt",
  "Slow Down": "arrow-left",
  "Speed Past": "tachometer-alt",
  "Go Around": "redo",
  "Oh...": "circle",
  "Dock": "dollar-sign",
};

const STATUS2COMPONENT = [
  { component: () => ORION_STATUS_START },
  { component: () => ORION_STATUS_INSTRUCTIONS },
  { component: () => ORION_STATUS_NORMAL },
  { component: () => ORION_STATUS_GAMEOVER },
  { component: () => ORION_STATUS_MARKET },
];

const locationInfo = [
  {
    title: "Pluto",
    blurb: "Pluto, long since occupied with long-range sensors and scanners, stands ready to, and indeed continues to probe the far reaches of the galaxy.",
  },
  {
    title: "Asteroid Belt",
    blurb: "At the edge of the Sol system lies a treacherous asteroid belt. Many have been crushed by stray asteroids and misguided judgement.",
  },
  {
    title: "Proxima Centauri",
    blurb: "The nearest star system to Sol, in ages past it stood as a reminder of the boundaries of sub-light travel, now a low-population sanctuary for adventurers and traders.",
  },
  {
    title: "Dead Space",
    blurb: "This region of space is particularly devoid of matter. Such low-density pockets are known to exist, but the vastness of it is astounding.",
  },
  {
    title: "Rigel Prime",
    blurb: "Rigel Prime, the center of the Rigel system, burns hot, basking its planetary bodies in warmth and radiation.",
  },
  {
    title: "Tau Ceti Beta",
    blurb: "Tau Ceti Beta has recently become a waypoint for colonists headed towards Orion. There are many ships and makeshift stations in the vicinity.",
  },
  {
    title: "Space Bugs",
    blurb: "You see some space bugs out your window. They contort in various reality bending ways, and it makes you sick. You know it's Galactic Policy to report all sightings of space bugs.",
  },
  {
    title: "Space Outpost Beta-9",
    blurb: "You have come into range of the first man-made structure in this region of space. It has been constructed not by travellers from Sol, but by colonists from Orion. It stands as a monument to the colonists' success.",
  },
  {
    title: "Orion Prime",
    blurb: "You have made it to Orion! Congratulations! Your crew is one of the few to start a new foothold for mankind!",
  },
];

const AdventureStatus = (props, context) => {
  const { data, act } = useBackend(context);
  const {
    lings_suspected,
    eventname,
    settlers,
    settlermoods,
    hull,
    electronics,
    engine,
    food,
    fuel,
  } = data;
  return (
    <Section
      title="Adventure Status"
      fill
      buttons={(
        !!lings_suspected && (
          <Button
            fluid
            color="black"
            textAlign="center"
            icon="skull"
            content="RANDOM KILL"
            disabled={eventname}
            onClick={() => act('random_kill')} />
        )
      )} >
      <Stack mb={-1} fill>
        <Stack.Item grow mb={-0.5}>
          {settlers?.map(settler => (
            <Stack key={settler}>
              <Stack.Item grow mt={0.9}>
                {settler}
              </Stack.Item>
              <Stack.Item mt={0.9}>
                <Button
                  fluid
                  color="red"
                  textAlign="center"
                  icon="skull"
                  content="KILL"
                  disabled={lings_suspected || eventname}
                  onClick={() => act('target_kill', {
                    who: settler,
                  })} />
              </Stack.Item>
              <Stack.Item mr={0}>
                <Box className={'moods32x32 mood' + (settlermoods[settler] + 1)} />
              </Stack.Item>
            </Stack>
          ))}
        </Stack.Item>
        <Divider vertical />
        <Stack.Item>
          <Stack vertical fill>
            <Stack.Item>
              <Button
                fluid
                icon="hamburger"
                content={"Food Left: " + food}
                color="green" />
            </Stack.Item>
            <Stack.Item>
              <Button
                fluid
                icon="gas-pump"
                content={"Fuel Left: " + fuel}
                color="olive" />
            </Stack.Item>
            <Stack.Item>
              <Button
                fluid
                icon="wrench"
                content={"Hull Parts: "+hull}
                color="average" />
            </Stack.Item>
            <Stack.Item>
              <Button
                fluid
                icon="server"
                content={"Electronics: "+electronics}
                color="blue" />
            </Stack.Item>
            <Stack.Item mb={1}>
              <Button
                fluid
                icon="rocket"
                content={"Engine Parts: "+engine}
                color="violet" />
            </Stack.Item>
          </Stack>
        </Stack.Item>
      </Stack>

  );
};

const ORION_STATUS_START = (props, context) => {
  const { data, act } = useBackend(context);
  const {
    gamename,
  } = data;
  return (

      <Stack vertical textAlign="center" fill>
        <Stack.Item grow={1} />
        <Stack.Item fontSize="32px">
          {gamename}
        </Stack.Item>
        <Stack.Item grow fontSize="15px" color="label">
          {"\"Experience the journey of your ancestors!\""}
        </Stack.Item>
        <Stack.Item fontSize="15px">
          <Button
            lineHeight={2}
            fluid
            icon="play"
            content="Begin Game"
            onClick={() => act('start_game')} />
        </Stack.Item>
        <Stack.Item fontSize="15px">
          <Button
            lineHeight={2}
            fluid
            icon="info"
            content="Instructions"
            onClick={() => act('instructions')} />
        </Stack.Item>
        <Stack.Item grow={3} />
      </Stack>

  );
};

const ORION_STATUS_INSTRUCTIONS = (props, context) => {
  const { act } = useBackend(context);
  const fake_settlers = ["John", "William", "Alice", "Tom"];
  return (
    <>
      <Section
        color="label"
        title="Objective"
        buttons={(
          <Button
            content="Back to Main Menu"
            onClick={() => act('back_to_menu')} />
        )}>
        <Box fontSize="11px">
          In the 2200's, the Orion trail was established as a dangerous
          yet opportunistic trail through space for those willing to risk it.
          Many pioneers seeking new lives on the galactic frontier would find
          exactly what they were seeking... or lose their lives on the way.
        </Box>

        <Stack mb={-1}>
          <Stack.Item basis={70} grow mb={-0.5}>
            {fake_settlers?.map(settler => (
              <Stack key={settler}>
                <Stack.Item grow mt={0.9}>
                  {settler}
                </Stack.Item>
                <Stack.Item mt={0.9}>
                  <Button
                    fluid
                    color="red"
                    textAlign="center"
                    icon="skull"
                    content="KILL" />
                </Stack.Item>
                <Stack.Item mr={0}>
                  <Box className={'moods32x32 mood5'} />
                </Stack.Item>
              </Stack>
            ))}
          </Stack.Item>
          <Divider vertical />
          <Stack.Item>
            This is the status panel for your pioneers. Each one requires
            1 food every time you continue
            towards Orion.
            You can find more crew on your journey, and lose them as
            fast as you found 'em.

            If you run out of food or crew,
            it's GAME OVER for you!
          </Stack.Item>
        </Stack>

        <Stack>
          <Stack.Item grow mt={-1}>
            If you want to make it to Orion,
            you'll need to manage your resources:

            Food: Your crewmembers consume
            it. More crew means this goes down faster!

            Fuel: You use 5u of fuel with
            every movement. Don't let it run out.

            Parts: Used to repair breakdowns.
            Nobody likes wasting time on repairs!
          </Stack.Item>
          <Divider vertical />
          <Stack.Item>
            <Stack vertical fill>
              <Stack.Item>
                <Button
                  fluid
                  icon="hamburger"
                  content={"Food Left: 80"}
                  color="green" />
              </Stack.Item>
              <Stack.Item>
                <Button
                  fluid
                  icon="gas-pump"
                  content={"Fuel Left: 60"}
                  color="olive" />
              </Stack.Item>
              <Stack.Item>
                <Button
                  fluid
                  icon="wrench"
                  content={"Hull Parts: 1"}
                  color="average" />
              </Stack.Item>
              <Stack.Item>
                <Button
                  fluid
                  icon="server"
                  content={"Electronics: 1"}
                  color="blue" />
              </Stack.Item>
              <Stack.Item mb={-0.3}>
                <Button
                  fluid
                  icon="rocket"
                  content={"Engine Parts: 1"}
                  color="violet" />
              </Stack.Item>
            </Stack>
          </Stack.Item>
        </Stack>

    </>
  );
};

const ORION_STATUS_NORMAL = (props, context) => {
  const { data, act } = useBackend(context);
  const {
    settlers,
    settlermoods,
    hull,
    electronics,
    engine,
    food,
    fuel,
    turns,
    eventname,
    eventtext,
    buttons,
  } = data;
  return (
    <Stack vertical fill>
      <Stack.Item grow>

          <Stack fill textAlign="center" vertical>
            <Stack.Item grow >
              <Box bold fontSize="15px">
                {!!eventname && eventname || locationInfo[turns-1].title}
              </Box>

              <Box fontSize="15px">
                {!!eventtext && eventtext || locationInfo[turns-1].blurb}
              </Box>
            </Stack.Item>
            <Stack.Item>
              {!!buttons && (
                buttons.map(button => (
                  <Stack.Item key={button}>
                    <Button
                      mb={1}
                      lineHeight={3}
                      width={16}
                      icon={variousButtonIcons[button]}
                      content={button}
                      onClick={() => act(button)} />
                  </Stack.Item>
                ))
              ) || (
                <Button
                  mb={1}
                  lineHeight={3}
                  width={16}
                  icon="arrow-right"
                  content="Continue"
                  onClick={() => act('continue')} />
              )}
            </Stack.Item>
          </Stack>

      </Stack.Item>
      <Stack.Item>
        <AdventureStatus />
      </Stack.Item>
    </Stack>
  );
};

const ORION_STATUS_GAMEOVER = (props, context) => {
  const { data, act } = useBackend(context);
  const {
    reason,
  } = data;
  return (

      <Stack vertical textAlign="center" fill>
        <Stack.Item grow={1} />
        <Stack.Item color="red" fontSize="32px">
          {"Game Over"}
        </Stack.Item>
        <Stack.Item grow fontSize="15px" color="label">
          {reason}
        </Stack.Item>
        <Stack.Item fontSize="15px">
          <Button
            lineHeight={2}
            fluid
            icon="arrow-left"
            content="Main Menu"
            onClick={() => act('back_to_menu')} />
        </Stack.Item>
        <Stack.Item grow={3} />
      </Stack>

  );
};

const marketButtonSpacing = 0.8;

const ORION_STATUS_MARKET = (props, context) => {
  const { data, act } = useBackend(context);
  const {
    turns,
    spaceport_raided,
  } = data;
  return (
    <Stack vertical fill>
      <Stack.Item grow>
        <Section
          title="Market"
          fill
          buttons={(
            <>
              <Button
                content="Raid"
                icon="skull"
                color="black"
                disabled={spaceport_raided}
                onClick={() => act('raid_spaceport')} />
              <Button
                content="Leave"
                icon="arrow-right"
                onClick={() => act('leave_spaceport')} />
            </>
          )}>
          <Stack fill textAlign="center" vertical>
            <Stack.Item grow >
              <Box mb={-2} bold fontSize="15px">
                {turns === 4 && "Tau Ceti Beta" || "Small Space Port"}
              </Box>

              <Box fontSize="14px">
                {spaceport_raided && (
                  <Box color="red">
                    You are lucky to have escaped with your life. Attempting
                    to dock again would be certain death.
                  </Box>
                ) || (
                  "Hello, Pioneer! We have supplies for you to help \
                  you reach Orion. They aren't free, though!"
                )}
              </Box>
            </Stack.Item>
            {spaceport_raided && (
              <>
                <Stack.Item>
                  The Port is under high security. Any possibility of
                  purchasing goods has long since sailed.
                </Stack.Item>
                <Stack.Item grow />
              </>
            ) || (
              <>
                <Stack.Item>
                  General Markets:
                </Stack.Item>
                <Stack.Item>
                  <Stack mb={-1} fill>
                    <Stack.Item grow basis={0}>
                      <Stack vertical>
                        <Stack.Item>
                          <Button
                            fluid
                            icon="gas-pump"
                            content={"5 Food -> 5 Fuel"}
                            color="green"
                            onClick={() => act('trade', {
                              what: 2,
                            })} />
                        </Stack.Item>
                        <Divider />
                        <Stack.Item mt={0}>
                          Port Hangar Bay:
                        </Stack.Item>
                        <Stack.Item mb={marketButtonSpacing}>
                          <Button
                            fluid
                            icon="wrench"
                            content={"5 Fuel for Hull Plates"}
                            color="average"
                            onClick={() => act('buyparts', {
                              part: 2,
                            })} />
                        </Stack.Item>
                        <Stack.Item mb={marketButtonSpacing}>
                          <Button
                            fluid
                            icon="server"
                            content={"5 Fuel for Electronics"}
                            color="blue"
                            onClick={() => act('buyparts', {
                              part: 3,
                            })} />
                        </Stack.Item>
                        <Stack.Item mb={marketButtonSpacing}>
                          <Button
                            fluid
                            icon="rocket"
                            content={"5 Fuel for Engine Parts"}
                            color="violet"
                            onClick={() => act('buyparts', {
                              part: 1,
                            })} />
                        </Stack.Item>
                      </Stack>
                    </Stack.Item>
                    <Stack.Item grow basis={0}>
                      <Stack vertical>
                        <Stack.Item>
                          <Button
                            fluid
                            icon="hamburger"
                            content={"5 Fuel -> 5 Food"}
                            color="olive"
                            onClick={() => act('trade', {
                              what: 1,
                            })} />
                        </Stack.Item>
                        <Divider />
                        <Stack.Item mt={0}>
                          Port Bar:
                        </Stack.Item>
                        <Stack.Item mb={marketButtonSpacing}>
                          <Button
                            fluid
                            icon="user-plus"
                            content={"10 Food, 10 Fuel for Crew"}
                            color="white"
                            onClick={() => act('buycrew')} />
                        </Stack.Item>
                        <Stack.Item mb={marketButtonSpacing}>
                          <Button
                            fluid
                            icon="user-minus"
                            content={"Crew for 7 Food, 7 Fuel"}
                            color="black"
                            onClick={() => act('sellcrew')} />
                        </Stack.Item>
                        <Stack.Item mb={marketButtonSpacing}>
                          <Button
                            fluid
                            icon="meteor"
                            content={"Odd Crew (Same Price)"}
                            color="purple"
                            onClick={() => act('buycrew', {
                              odd: 1,
                            })} />
                        </Stack.Item>
                      </Stack>
                    </Stack.Item>
                  </Stack>
                </Stack.Item>
              </>
            )}

          </Stack>

      </Stack.Item>
      <Stack.Item>
        <AdventureStatus />
      </Stack.Item>
    </Stack>
  );
};

export const OrionGame = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    gamestatus,
    gamename,
    eventname,
  } = data;
  const GameStatusComponent = STATUS2COMPONENT[gamestatus].component();
  const MarketRaid = STATUS2COMPONENT[2].component();
  return (
    <Window
      title={gamename}
      width={420}
      height={510}>
      <Window.Content scrollable>
        {eventname === "Space Port Raid" && (
          <MarketRaid />
        ) || (
          <GameStatusComponent />
        )}
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend } from '../backend';
import { Box, Button, Icon, Section, Stack } from '../components';
import { Window } from '../layouts';

export const OutfitEditor = (props, context) => {
  const { act, data } = useBackend(context);
  const { outfit, saveable, dummy64 } = data;
  return (
    <Window
      width={380}
      height={600}
      theme="admin">
      <Window.Content>
        <Box
          as="img"
          fillPositionedParent
          width="100%"
          height="100%"
          opacity={0.5}
          py={3}
          src={`data:image/jpeg;base64,${dummy64}`}
          style={{
            '-ms-interpolation-mode': 'nearest-neighbor',
          }} />
        <Section
          fill
          title={
            <Stack>
              <Stack.Item grow={1}
                style={{
                  'overflow': 'hidden',
                  'white-space': 'nowrap',
                  'text-overflow': 'ellipsis',
                }}>
                <Button
                  ml={0.5}
                  color="transparent"
                  icon="pencil-alt"
                  title="Rename this outfit"
                  onClick={() => act("rename", {})} />
                {outfit.name}
              </Stack.Item>
              <Stack.Item align="end" shrink={0}>
                <Button
                  color="transparent"
                  icon="info"
                  tooltip="Ctrl-click a button to select *any* item instead of what will probably fit in that slot."
                  tooltipPosition="bottom-start" />
                <Button
                  icon="code"
                  tooltip="Edit this outfit on a VV window"
                  tooltipPosition="bottom-start"
                  onClick={() => act("vv")} />
                <Button
                  color={!saveable && "bad"}
                  icon={saveable ? "save" : "trash-alt"}
                  tooltip={saveable
                    ? "Save this outfit to the custom outfit list"
                    : "Remove this outfit from the custom outfit list"}
                  tooltipPosition="bottom-start"
                  onClick={() => act(saveable ? "save" : "delete")} />
              </Stack.Item>
            </Stack>
          }>
          <Box textAlign="center">
            <Stack mb={2}>
              <OutfitSlot name="Headgear" icon="hard-hat" slot="head" />
              <OutfitSlot name="Glasses" icon="glasses" slot="glasses" />
              <OutfitSlot name="Ears" icon="headphones-alt" slot="ears" />
            </Stack>
            <Stack mb={2}>
              <OutfitSlot name="Neck" icon="stethoscope" slot="neck" />
              <OutfitSlot name="Mask" icon="theater-masks" slot="mask" />
            </Stack>
            <Stack mb={2}>
              <OutfitSlot name="Uniform" icon="tshirt" slot="uniform" />
              <OutfitSlot name="Suit" icon="user-tie" slot="suit" />
              <OutfitSlot name="Gloves" icon="mitten" slot="gloves" />
            </Stack>
            <Stack mb={2}>
              <OutfitSlot name="Suit Storage" icon="briefcase-medical" slot="suit_store" />
              <OutfitSlot name="Back" icon="shopping-bag" slot="back" />
              <OutfitSlot name="ID" icon="id-card-o" slot="id" />
            </Stack>
            <Stack mb={2}>
              <OutfitSlot name="Belt" icon="band-aid" slot="belt" />
              <OutfitSlot name="Left Hand" icon="hand-paper" slot="l_hand" />
              <OutfitSlot name="Right Hand" icon="hand-paper" slot="r_hand" />
            </Stack>
            <Stack mb={2}>
              <OutfitSlot name="Shoes" icon="socks" slot="shoes" />
              <OutfitSlot name="Left Pocket" icon="envelope-open-o" iconRot={180} slot="l_pocket" />
              <OutfitSlot name="Right Pocket" icon="envelope-open-o" iconRot={180} slot="r_pocket" />
            </Stack>
          </Box>
        </Section>
      </Window.Content>
    </Window>
  );
};

const OutfitSlot = (props, context) => {
  const { act, data } = useBackend(context);
  const { name, icon, iconRot, slot } = props;
  const { outfit } = data;
  const currItem = outfit[slot];
  return (
    <Stack.Item grow={1} basis={0}>
      <Button fluid height={2}
        bold
        // todo: intuitive way to clear items
        onClick={e => act(e.ctrlKey ? "ctrlClick" : "click", { slot })} >
        <Icon name={icon} rotation={iconRot} />
        {name}
      </Button>
      <Box height="32px">
        {currItem?.sprite && (
          <>
            <Box
              as="img"
              src={`data:image/jpeg;base64,${currItem?.sprite}`}
              title={currItem?.desc}
              style={{
                '-ms-interpolation-mode': 'nearest-neighbor',
              }} />
            <Icon
              position="absolute"
              name="times"
              color="label"
              style={{ cursor: 'pointer' }}
              onClick={() => act("clear", { slot })} />
          </>
        )}
      </Box>
      <Box
        color="label"
        style={{
          'overflow': 'hidden',
          'white-space': 'nowrap',
          'text-overflow': 'ellipsis',
        }}
        title={currItem?.path}>
        {currItem?.name || "Empty"}
      </Box>
    </Stack.Item>
  );
};


code:
import { useBackend } from '../backend';
import { Button, Section, Stack } from '../components';
import { Window } from '../layouts';

export const OutfitManager = (props, context) => {
  const { act, data } = useBackend(context);
  const { outfits } = data;
  return (
    <Window
      title="Outfit Manager"
      width={300}
      height={300}
      theme="admin">
      <Window.Content>
        <Section
          fill
          scrollable
          title="Custom Outfit Manager"
          buttons={
            <>
              <Button
                icon="file-upload"
                tooltip="Load an outfit from a file"
                tooltipPosition="left"
                onClick={() => act("load")} />
              <Button
                icon="copy"
                tooltip="Copy an already existing outfit"
                tooltipPosition="left"
                onClick={() => act("copy")} />
              <Button
                icon="plus"
                tooltip="Create a new outfit"
                tooltipPosition="left"
                onClick={() => act("new")} />
            </>
          }>
          <Stack vertical>
            {outfits?.map(outfit => (
              <Stack.Item key={outfit.ref}>
                <Stack>
                  <Stack.Item grow={1} shrink={1}
                    style={{
                      'overflow': 'hidden',
                      'white-space': 'nowrap',
                      'text-overflow': 'ellipsis',
                    }}>
                    <Button
                      fluid
                      style={{
                        'overflow': 'hidden',
                        'white-space': 'nowrap',
                        'text-overflow': 'ellipsis',
                      }}
                      content={outfit.name}
                      onClick={() => act("edit", { outfit: outfit.ref })} />
                  </Stack.Item>
                  <Stack.Item ml={0.5}>
                    <Button
                      icon="save"
                      tooltip="Save this outfit to a file"
                      tooltipPosition="left"
                      onClick={() => act("save", { outfit: outfit.ref })} />
                  </Stack.Item>
                  <Stack.Item ml={0.5}>
                    <Button
                      color="bad"
                      icon="trash-alt"
                      tooltip="Delete this outfit"
                      tooltipPosition="left"
                      onClick={() => act("delete", { outfit: outfit.ref })} />
                  </Stack.Item>
                </Stack>
              </Stack.Item>
            ))}
          </Stack>
        </Section>
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend, useSharedState } from '../backend';
import { Window } from '../layouts';
import { Button, Dropdown, Section, Stack } from '../components';

export const PaintingMachine = (props, context) => {
  const { act, data } = useBackend(context);

  const {
    pdaTypes,
    cardTrims,
    hasPDA,
    pdaName,
    hasID,
    idName,
  } = data;

  const [
    selectedPDA,
  ] = useSharedState(context, "pdaSelection", pdaTypes[Object.keys(pdaTypes)[0]]);

  const [
    selectedTrim,
  ] = useSharedState(context, "trimSelection", cardTrims[Object.keys(cardTrims)[0]]);

  return (
    <Window
      width={500}
      height={620}>
      <Window.Content scrollable>
        <Section
          title="PDA Painter"
          buttons={
            <Button.Confirm
              disabled={!hasPDA}
              content="Paint PDA"
              confirmContent="Confirm?"
              onClick={() => act("trim_pda", {
                selection: selectedPDA,
              })} />
          }>
          <Stack vertical>
            <Stack.Item height="100%">
              <EjectButton
                name={pdaName || "-----"}
                onClickEject={() => act("eject_pda")} />
            </Stack.Item>
            <Stack.Item height="100%">
              <PainterDropdown
                stateKey="pdaSelection"
                options={pdaTypes} />
            </Stack.Item>
          </Stack>
        </Section>
        <Section
          title="ID Trim Imprinter"
          buttons={
            <>
              <Button.Confirm
                disabled={!hasID}
                content="Reset ID Account"
                confirmContent="Confirm?"
                onClick={() => act("reset_card")} />
              <Button.Confirm
                disabled={!hasID}
                content="Imprint ID Trim"
                confirmContent="Confirm?"
                onClick={sel => act("trim_card", {
                  selection: selectedTrim,
                })} />
              <Button
                icon="question-circle"
                tooltip={"WARNING: This is destructive"
                + " and will wipe ALL access on the card."}
                tooltipPosition="left" />
            </>
          }>
          <Stack vertical>
            <Stack.Item height="100%">
              <EjectButton
                name={idName || "-----"}
                onClickEject={() => act("eject_card")} />
            </Stack.Item>
            <Stack.Item height="100%">
              <PainterDropdown
                stateKey="trimSelection"
                options={cardTrims} />
            </Stack.Item>
          </Stack>
        </Section>
      </Window.Content>
    </Window>
  );
};

export const EjectButton = (props, context) => {
  const {
    name,
    onClickEject,
  } = props;

  return (
    <Button
      fluid
      ellipsis
      icon="eject"
      content={name}
      onClick={() => onClickEject()} />
  );
};

export const PainterDropdown = (props, context) => {
  const {
    stateKey,
    options,
  } = props;

  const [
    selectedOption,
    setSelectedOption,
  ] = useSharedState(context, stateKey, options[Object.keys(options)[0]]);

  return (
    <Dropdown
      width="100%"
      selected={selectedOption}
      options={
        Object.keys(options).map(path => {
          return options[path];
        })
      }
      onSelected={sel => setSelectedOption(sel)} />
  );
};


code:
import { map } from 'common/collections';
import { useBackend } from '../backend';
import { Box, Button, Collapsible, Grid, Input, LabeledList, NoticeBox, Section } from '../components';
import { Window } from '../layouts';

export const PandemicBeakerDisplay = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    has_beaker,
    beaker_empty,
    has_blood,
    blood,
  } = data;
  const cant_empty = !has_beaker || beaker_empty;
  return (
    <Section
      title="Beaker"
      buttons={(
        <>
          <Button
            icon="times"
            content="Empty and Eject"
            color="bad"
            disabled={cant_empty}
            onClick={() => act('empty_eject_beaker')} />
          <Button
            icon="trash"
            content="Empty"
            disabled={cant_empty}
            onClick={() => act('empty_beaker')} />
          <Button
            icon="eject"
            content="Eject"
            disabled={!has_beaker}
            onClick={() => act('eject_beaker')} />
        </>
      )}>
      {has_beaker ? (
        !beaker_empty ? (
          has_blood ? (
            <LabeledList>
              <LabeledList.Item label="Blood DNA">
                {(blood && blood.dna) || 'Unknown'}
              </LabeledList.Item>
              <LabeledList.Item label="Blood Type">
                {(blood && blood.type) || 'Unknown'}
              </LabeledList.Item>
            </LabeledList>
          ) : (
            <Box color="bad">
              No blood detected
            </Box>
          )
        ) : (
          <Box color="bad">
            Beaker is empty
          </Box>
        )
      ) : (
        <NoticeBox>
          No beaker loaded
        </NoticeBox>
      )}

  );
};

export const PandemicDiseaseDisplay = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    is_ready,
  } = data;
  const viruses = data.viruses || [];
  return (
    viruses.map(virus => {
      const symptoms = virus.symptoms || [];
      return (
        <Section
          key={virus.name}
          title={virus.can_rename ? (
            <Input
              value={virus.name}
              onChange={(e, value) => act('rename_disease', {
                index: virus.index,
                name: value,
              })} />
          ) : (
            virus.name
          )}
          buttons={(
            <Button
              icon="flask"
              content="Create culture bottle"
              disabled={!is_ready}
              onClick={() => act('create_culture_bottle', {
                index: virus.index,
              })} />
          )}>
          <Grid>
            <Grid.Column>
              {virus.description}
            </Grid.Column>
            <Grid.Column>
              <LabeledList>
                <LabeledList.Item label="Agent">
                  {virus.agent}
                </LabeledList.Item>
                <LabeledList.Item label="Spread">
                  {virus.spread}
                </LabeledList.Item>
                <LabeledList.Item label="Possible Cure">
                  {virus.cure}
                </LabeledList.Item>
              </LabeledList>
            </Grid.Column>
          </Grid>
          {!!virus.is_adv && (
            <>

                <Grid>
                  <Grid.Column>
                    <LabeledList>
                      <LabeledList.Item label="Resistance">
                        {virus.resistance}
                      </LabeledList.Item>
                      <LabeledList.Item label="Stealth">
                        {virus.stealth}
                      </LabeledList.Item>
                    </LabeledList>
                  </Grid.Column>
                  <Grid.Column>
                    <LabeledList>
                      <LabeledList.Item label="Stage speed">
                        {virus.stage_speed}
                      </LabeledList.Item>
                      <LabeledList.Item label="Transmissibility">
                        {virus.transmission}
                      </LabeledList.Item>
                    </LabeledList>
                  </Grid.Column>
                </Grid>

                {symptoms.map(symptom => (
                  <Collapsible
                    key={symptom.name}
                    title={symptom.name}>

                      <PandemicSymptomDisplay symptom={symptom} />

                  </Collapsible>
                ))}

            </>
          )}

      );
    })
  );
};

export const PandemicSymptomDisplay = (props, context) => {
  const { symptom } = props;
  const {
    name,
    desc,
    stealth,
    resistance,
    stage_speed,
    transmission,
    level,
    neutered,
  } = symptom;
  const thresholds = map((desc, label) => ({ desc, label }))(
    symptom.threshold_desc || {});
  return (
    <Section
      title={name}
      level={2}
      buttons={!!neutered && (
        <Box
          bold
          color="bad">
          Neutered
        </Box>
      )}>
      <Grid>
        <Grid.Column size={2}>
          {desc}
        </Grid.Column>
        <Grid.Column>
          <LabeledList>
            <LabeledList.Item label="Level">
              {level}
            </LabeledList.Item>
            <LabeledList.Item label="Resistance">
              {resistance}
            </LabeledList.Item>
            <LabeledList.Item label="Stealth">
              {stealth}
            </LabeledList.Item>
            <LabeledList.Item label="Stage Speed">
              {stage_speed}
            </LabeledList.Item>
            <LabeledList.Item label="Transmission">
              {transmission}
            </LabeledList.Item>
          </LabeledList>
        </Grid.Column>
      </Grid>
      {thresholds.length > 0 && (

          <LabeledList>
            {thresholds.map(threshold => {
              return (
                <LabeledList.Item
                  key={threshold.label}
                  label={threshold.label}>
                  {threshold.desc}
                </LabeledList.Item>
              );
            })}
          </LabeledList>

      )}

  );
};

export const PandemicAntibodyDisplay = (props, context) => {
  const { act, data } = useBackend(context);
  const resistances = data.resistances || [];
  return (

      {resistances.length > 0 ? (
        <LabeledList>
          {resistances.map(resistance => (
            <LabeledList.Item
              key={resistance.name}
              label={resistance.name}>
              <Button
                icon="eye-dropper"
                content="Create vaccine bottle"
                disabled={!data.is_ready}
                onClick={() => act('create_vaccine_bottle', {
                  index: resistance.id,
                })} />
            </LabeledList.Item>
          ))}
        </LabeledList>
      ) : (
        <Box
          bold
          color="bad"
          mt={1}>
          No antibodies detected.
        </Box>
      )}

  );
};

export const Pandemic = (props, context) => {
  const { data } = useBackend(context);
  return (
    <Window
      width={520}
      height={550}>
      <Window.Content scrollable>
        <PandemicBeakerDisplay />
        {!!data.has_blood && (
          <>
            <PandemicDiseaseDisplay />
            <PandemicAntibodyDisplay />
          </>
        )}
      </Window.Content>
    </Window>
  );
};


code:
/**
 * @file
 * @copyright 2020 WarlockD (https://github.com/warlockd)
 * @author Original WarlockD (https://github.com/warlockd)
 * @author Changes stylemistake
 * @author Changes ThePotato97
 * @author Changes Ghommie
 * @license MIT
 */

import { classes } from 'common/react';
import { Component } from 'inferno';
import marked from 'marked';
import { useBackend } from '../backend';
import { Box, Flex, Tabs, TextArea } from '../components';
import { Window } from '../layouts';
import { clamp } from 'common/math';
import { sanitizeText } from '../sanitize';
const MAX_PAPER_LENGTH = 5000; // Question, should we send this with ui_data?

// Hacky, yes, works?...yes
const textWidth = (text, font, fontsize) => {
  // default font height is 12 in tgui
  font = fontsize + "x " + font;
  const c = document.createElement('canvas');
  const ctx = c.getContext("2d");
  ctx.font = font;
  const width = ctx.measureText(text).width;
  return width;
};

const setFontinText = (text, font, color, bold=false) => {
  return "" + text + "";
};

const createIDHeader = index => {
  return "paperfield_" + index;
};
// To make a field you do a [_______] or however long the field is
// we will then output a TEXT input for it that hopefully covers
// the exact amount of spaces
const field_regex = /\[(_+)\]/g;
const field_tag_regex = /\[<input\s+(?!disabled)(.*?)\s+id="(?<id>paperfield_\d+)"(.*?)\/>\]/gm;
const sign_regex = /%s(?:ign)?(?=\\s|$)?/igm;

const createInputField = (length, width, font,
  fontsize, color, id) => {
  return "[<input "
      + "type=\"text\" "
      + "style=\""
      + "font:'" + fontsize + "x " + font + "';"
      + "color:" + color + ";"
      + "min-width:" + width + ";"
      + "max-width:" + width + ";"
      + "\" "
      + "id=\"" + id + "\" "
      + "maxlength=" + length +" "
      + "size=" + length + " "
      + "/>]";
};

const createFields = (txt, font, fontsize, color, counter) => {
  const ret_text = txt.replace(field_regex, (match, p1, offset, string) => {
    const width = textWidth(match, font, fontsize) + "px";
    return createInputField(p1.length,
      width, font, fontsize, color, createIDHeader(counter++));
  });
  return {
    counter,
    text: ret_text,
  };
};

const signDocument = (txt, color, user) => {
  return txt.replace(sign_regex, () => {
    return setFontinText(user, "Times New Roman", color, true);
  });
};

const run_marked_default = value => {
  // Override function, any links and images should
  // kill any other marked tokens we don't want here
  const walkTokens = token => {
    switch (token.type) {
      case 'url':
      case 'autolink':
      case 'reflink':
      case 'link':
      case 'image':
        token.type = 'text';
        // Once asset system is up change to some default image
        // or rewrite for icon images
        token.href = "";
        break;
    }
  };
  return marked(value, {
    breaks: true,
    smartypants: true,
    smartLists: true,
    walkTokens,
    // Once assets are fixed might need to change this for them
    baseUrl: 'thisshouldbreakhttp',
  });
};

/*
** This gets the field, and finds the dom object and sees if
** the user has typed something in.  If so, it replaces,
** the dom object, in txt with the value, spaces so it
** fits the [] format and saves the value into a object
** There may be ways to optimize this in javascript but
** doing this in byond is nightmarish.
**
** It returns any values that were saved and a corrected
** html code or null if nothing was updated
*/
const checkAllFields = (txt, font, color, user_name, bold=false) => {
  let matches;
  let values = {};
  let replace = [];
  // I know its tempting to wrap ALL this in a .replace
  // HOWEVER the user might not of entered anything
  // if thats the case we are rebuilding the entire string
  // for nothing, if nothing is entered, txt is just returned
  while ((matches = field_tag_regex.exec(txt)) !== null) {
    const full_match = matches[0];
    const id = matches.groups.id;
    if (id) {
      const dom = document.getElementById(id);
      // make sure we got data, and kill any html that might
      // be in it
      const dom_text = dom && dom.value ? dom.value : "";
      if (dom_text.length === 0) {
        continue;
      }
      const sanitized_text = sanitizeText(dom.value.trim(), []);
      if (sanitized_text.length === 0) {
        continue;
      }
      // this is easier than doing a bunch of text manipulations
      const target = dom.cloneNode(true);
      // in case they sign in a field
      if (sanitized_text.match(sign_regex)) {
        target.style.fontFamily = "Times New Roman";
        bold = true;
        target.defaultValue = user_name;
      }
      else {
        target.style.fontFamily = font;
        target.defaultValue = sanitized_text;
      }
      if (bold) {
        target.style.fontWeight = "bold";
      }
      target.style.color = color;
      target.disabled = true;
      const wrap = document.createElement('div');
      wrap.appendChild(target);
      values[id] = sanitized_text; // save the data
      replace.push({ value: "[" + wrap.innerHTML + "]", raw_text: full_match });
    }
  }
  if (replace.length > 0) {
    for (const o of replace) {

      txt = txt.replace(o.raw_text, o.value);
    }
  }
  return { text: txt, fields: values };
};

const pauseEvent = e => {
  if (e.stopPropagation) { e.stopPropagation(); }
  if (e.preventDefault) { e.preventDefault(); }
  e.cancelBubble=true;
  e.returnValue=false;
  return false;
};

const Stamp = (props, context) => {
  const {
    image,
    opacity,
  } = props;
  const stamp_transform = {
    'left': image.x + 'px',
    'top': image.y + 'px',
    'transform': 'rotate(' + image.rotate + 'deg)',
    'opacity': opacity || 1.0,
  };
  return (

  );
};

const setInputReadonly = (text, readonly) => {
  return readonly
    ? text.replace(/<input\s[^d]/g, '<input disabled ')
    : text.replace(/<input\sdisabled\s/g, '<input ');
};

// got to make this a full component if we
// want to control updates
const PaperSheetView = (props, context) => {
  const {
    value = "",
    stamps = [],
    backgroundColor,
    readOnly,
  } = props;
  const stamp_list = stamps;
  const text_html = {
    __html: ''
      + setInputReadonly(value, readOnly)
      + '',
  };
  return (
    <Box
      position="relative"
      backgroundColor={backgroundColor}
      width="100%"
      height="100%" >
      <Box
        className="Paper__Page"
        fillPositionedParent
        width="100%"
        height="100%"
        dangerouslySetInnerHTML={text_html}
        p="10px" />
      {stamp_list.map((o, i) => (
        <Stamp key={o[0] + i}
          image={{ sprite: o[0], x: o[1], y: o[2], rotate: o[3] }} />
      ))}
    </Box>
  );
};

// again, need the states for dragging and such
class PaperSheetStamper extends Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      x: 0,
      y: 0,
      rotate: 0,
    };
    this.style = null;
    this.handleMouseMove = e => {
      const pos = this.findStampPosition(e);
      if (!pos) { return; }
      // center offset of stamp & rotate
      pauseEvent(e);
      this.setState({ x: pos[0], y: pos[1], rotate: pos[2] });
    };
    this.handleMouseClick = e => {
      if (e.pageY <= 30) { return; }
      const { act, data } = useBackend(this.context);
      const stamp_obj = {
        x: this.state.x, y: this.state.y, r: this.state.rotate,
        stamp_class: this.props.stamp_class,
        stamp_icon_state: data.stamp_icon_state,
      };
      act("stamp", stamp_obj);
    };
  }

  findStampPosition(e) {
    let rotating;
    const windowRef = document.querySelector('.Layout__content');
    if (e.shiftKey) {
      rotating = true;
    }

    if (document.getElementById("stamp"))
    {
      const stamp = document.getElementById("stamp");
      const stampHeight = stamp.clientHeight;
      const stampWidth = stamp.clientWidth;

      const currentHeight = rotating ? this.state.y : e.pageY
      - windowRef.scrollTop - stampHeight;
      const currentWidth = rotating ? this.state.x : e.pageX - (stampWidth / 2);

      const widthMin = 0;
      const heightMin = 0;

      const widthMax = (windowRef.clientWidth) - (
        stampWidth);
      const heightMax = (windowRef.clientHeight - windowRef.scrollTop) - (
        stampHeight);

      const radians = Math.atan2(
        e.pageX - currentWidth,
        e.pageY - currentHeight
      );

      const rotate = rotating ? (radians * (180 / Math.PI) * -1)
        : this.state.rotate;

      const pos = [
        clamp(currentWidth, widthMin, widthMax),
        clamp(currentHeight, heightMin, heightMax),
        rotate,
      ];
      return pos;
    }
  }

  componentDidMount() {
    document.addEventListener("mousemove", this.handleMouseMove);
    document.addEventListener("click", this.handleMouseClick);
  }

  componentWillUnmount() {
    document.removeEventListener("mousemove", this.handleMouseMove);
    document.removeEventListener("click", this.handleMouseClick);
  }

  render() {
    const {
      value,
      stamp_class,
      stamps,
    } = this.props;
    const stamp_list = stamps || [];
    const current_pos = {
      sprite: stamp_class,
      x: this.state.x,
      y: this.state.y,
      rotate: this.state.rotate,
    };
    return (
      <>
        <PaperSheetView
          readOnly
          value={value}
          stamps={stamp_list} />
        <Stamp
          active_stamp
          opacity={0.5} image={current_pos} />
      </>
    );
  }
}

// This creates the html from marked text as well as the form fields
const createPreview = (
  value,
  text,
  do_fields = false,
  field_counter,
  color,
  font,
  user_name,
  is_crayon = false,
) => {
  const out = { text: text };
  // check if we are adding to paper, if not
  // we still have to check if someone entered something
  // into the fields
  value = value.trim();
  if (value.length > 0) {
    // First lets make sure it ends in a new line
    value += value[value.length] === "\n" ? " \n" : "\n \n";
    // Second, we sanitize the text of html
    const sanitized_text = sanitizeText(value);
    const signed_text = signDocument(sanitized_text, color, user_name);
    // Third we replace the [__] with fields as markedjs fucks them up
    const fielded_text = createFields(
      signed_text, font, 12, color, field_counter);
    // Fourth, parse the text using markup
    const formatted_text = run_marked_default(fielded_text.text);
    // Fifth, we wrap the created text in the pin color, and font.
    // crayon is bold ( tags), maybe make fountain pin italic?
    const fonted_text = setFontinText(
      formatted_text, font, color, is_crayon);
    out.text += fonted_text;
    out.field_counter = fielded_text.counter;
  }
  if (do_fields) {
    // finally we check all the form fields to see
    // if any data was entered by the user and
    // if it was return the data and modify the text
    const final_processing = checkAllFields(
      out.text, font, color, user_name, is_crayon);
    out.text = final_processing.text;
    out.form_fields = final_processing.fields;
  }
  return out;
};

// ugh.  So have to turn this into a full
// component too if I want to keep updates
// low and keep the weird flashing down
class PaperSheetEdit extends Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      previewSelected: "Preview",
      old_text: props.value || "",
      counter: props.counter || 0,
      textarea_text: "",
      combined_text: props.value || "",
    };
  }

  createPreviewFromData(value, do_fields = false) {
    const { data } = useBackend(this.context);
    return createPreview(value,
      this.state.old_text,
      do_fields,
      this.state.counter,
      data.pen_color,
      data.pen_font,
      data.edit_usr,
      data.is_crayon,
    );
  }
  onInputHandler(e, value) {
    if (value !== this.state.textarea_text) {
      const combined_length = this.state.old_text.length
        + this.state.textarea_text.length;
      if (combined_length > MAX_PAPER_LENGTH) {
        if ((combined_length - MAX_PAPER_LENGTH) >= value.length) {
          // Basically we cannot add any more text to the paper
          value = '';
        } else {
          value = value.substr(0, value.length
            - (combined_length - MAX_PAPER_LENGTH));
        }
        // we check again to save an update
        if (value === this.state.textarea_text) {
          // Do nothing
          return;
        }
      }
      this.setState(() => ({
        textarea_text: value,
        combined_text: this.createPreviewFromData(value),
      }));
    }
  }
  // the final update send to byond, final upkeep
  finalUpdate(new_text) {
    const { act } = useBackend(this.context);
    const final_processing = this.createPreviewFromData(new_text, true);
    act('save', final_processing);
    this.setState(() => { return {
      textarea_text: "",
      previewSelected: "save",
      combined_text: final_processing.text,
      old_text: final_processing.text,
      counter: final_processing.field_counter,
    }; });
    // byond should switch us to readonly mode from here
  }

  render() {
    const {
      textColor,
      fontFamily,
      stamps,
      backgroundColor,
    } = this.props;
    return (
      <Flex
        direction="column"
        fillPositionedParent>
        <Flex.Item>
          <Tabs>
            <Tabs.Tab
              key="marked_edit"
              textColor={'black'}
              backgroundColor={this.state.previewSelected === "Edit"
                ? "grey"
                : "white"}
              selected={this.state.previewSelected === "Edit"}
              onClick={() => this.setState({ previewSelected: "Edit" })}>
              Edit
            </Tabs.Tab>
            <Tabs.Tab
              key="marked_preview"
              textColor={'black'}
              backgroundColor={this.state.previewSelected === "Preview"
                ? "grey"
                : "white"}
              selected={this.state.previewSelected === "Preview"}
              onClick={() => this.setState(() => {
                const new_state = {
                  previewSelected: "Preview",
                  textarea_text: this.state.textarea_text,
                  combined_text: this.createPreviewFromData(
                    this.state.textarea_text).text,
                };
                return new_state;
              })}>
              Preview
            </Tabs.Tab>
            <Tabs.Tab
              key="marked_done"
              textColor={'black'}
              backgroundColor={this.state.previewSelected === "confirm"
                ? "red"
                : this.state.previewSelected === "save"
                  ? "grey"
                  : "white"}
              selected={this.state.previewSelected === "confirm"
                || this.state.previewSelected === "save"}
              onClick={() => {
                if (this.state.previewSelected === "confirm") {
                  this.finalUpdate(this.state.textarea_text);
                }
                else if (this.state.previewSelected === "Edit") {
                  this.setState(() => {
                    const new_state = {
                      previewSelected: "confirm",
                      textarea_text: this.state.textarea_text,
                      combined_text: this.createPreviewFromData(
                        this.state.textarea_text).text,
                    };
                    return new_state;
                  });
                }
                else {
                  this.setState({ previewSelected: "confirm" });
                }
              }}>
              {this.state.previewSelected === "confirm" ? "Confirm" : "Save"}
            </Tabs.Tab>
          </Tabs>
        </Flex.Item>
        <Flex.Item
          grow={1}
          basis={1}>
          {this.state.previewSelected === "Edit" && (
            <TextArea
              value={this.state.textarea_text}
              textColor={textColor}
              fontFamily={fontFamily}
              height={(window.innerHeight - 80) + "px"}
              backgroundColor={backgroundColor}
              onInput={this.onInputHandler.bind(this)} />
          ) || (
            <PaperSheetView
              value={this.state.combined_text}
              stamps={stamps}
              fontFamily={fontFamily}
              textColor={textColor} />
          )}
        </Flex.Item>
      </Flex>
    );
  }
}

export const PaperSheet = (props, context) => {
  const { data } = useBackend(context);
  const {
    edit_mode,
    text,
    paper_color = "white",
    pen_color = "black",
    pen_font = "Verdana",
    stamps,
    stamp_class,
    sizeX,
    sizeY,
    name,
    add_text,
    add_font,
    add_color,
    add_sign,
    field_counter,
  } = data;
  // some features can add text to a paper sheet outside of this ui
  // we need to parse, sanitize and add any of it to the text value.
  const values = { text: text, field_counter: field_counter };
  if (add_text) {
    for (let index = 0; index < add_text.length; index++) {
      const used_color = add_color[index];
      const used_font = add_font[index];
      const used_sign = add_sign[index];
      const processing = createPreview(
        add_text[index],
        values.text,
        false,
        values.field_counter,
        used_color,
        used_font,
        used_sign
      );
      values.text = processing.text;
      values.field_counter = processing.field_counter;
    }
  }
  const stamp_list = !stamps
    ? []
    : stamps;
  const decide_mode = mode => {
    switch (mode) {
      case 0:
        return (
          <PaperSheetView
            value={values.text}
            stamps={stamp_list}
            readOnly />
        );
      case 1:
        return (
          <PaperSheetEdit
            value={values.text}
            counter={values.field_counter}
            textColor={pen_color}
            fontFamily={pen_font}
            stamps={stamp_list}
            backgroundColor={paper_color} />
        );
      case 2:
        return (
          <PaperSheetStamper
            value={values.text}
            stamps={stamp_list}
            stamp_class={stamp_class} />
        );
      default:
        return "ERROR ERROR WE CANNOT BE HERE!!";
    }
  };
  return (
    <Window
      title={name}
      theme="paper"
      width={sizeX || 400}
      height={sizeY || 500}>
      <Window.Content
        backgroundColor={paper_color}
        scrollable>
        <Box
          id="page"
          fitted
          fillPositionedParent>
          {decide_mode(edit_mode)}
        </Box>
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend, useLocalState } from '../backend';
import { Button, Dimmer, Flex, Icon, LabeledList, Section, Tabs } from '../components';
import { Window } from '../layouts';

export const PersonalCrafting = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    busy,
    display_craftable_only,
    display_compact,
  } = data;
  const crafting_recipes = data.crafting_recipes || {};
  // Sort everything into flat categories
  const categories = [];
  const recipes = [];
  for (let category of Object.keys(crafting_recipes)) {
    const subcategories = crafting_recipes[category];
    if ('has_subcats' in subcategories) {
      for (let subcategory of Object.keys(subcategories)) {
        if (subcategory === 'has_subcats') {
          continue;
        }
        // Push category
        categories.push({
          name: subcategory,
          category,
          subcategory,
        });
        // Push recipes
        const _recipes = subcategories[subcategory];
        for (let recipe of _recipes) {
          recipes.push({
            ...recipe,
            category: subcategory,
          });
        }
      }
      continue;
    }
    // Push category
    categories.push({
      name: category,
      category,
    });
    // Push recipes
    const _recipes = crafting_recipes[category];
    for (let recipe of _recipes) {
      recipes.push({
        ...recipe,
        category,
      });
    }
  }
  // Sort out the tab state
  const [tab, setTab] = useLocalState(
    context, 'tab', categories[0]?.name);
  const shownRecipes = recipes
    .filter(recipe => recipe.category === tab);
  return (
    <Window
      title="Crafting Menu"
      width={700}
      height={800}>
      <Window.Content scrollable>
        {!!busy && (
          <Dimmer fontSize="32px">
            <Icon name="cog" spin={1} />
            {' Crafting...'}
          </Dimmer>
        )}
        <Section
          title="Personal Crafting"
          buttons={(
            <>
              <Button.Checkbox
                content="Compact"
                checked={display_compact}
                onClick={() => act('toggle_compact')} />
              <Button.Checkbox
                content="Craftable Only"
                checked={display_craftable_only}
                onClick={() => act('toggle_recipes')} />
            </>
          )}>
          <Flex>
            <Flex.Item>
              <Tabs vertical>
                {categories.map(category => (
                  <Tabs.Tab
                    key={category.name}
                    selected={category.name === tab}
                    onClick={() => {
                      setTab(category.name);
                      act('set_category', {
                        category: category.category,
                        subcategory: category.subcategory,
                      });
                    }}>
                    {category.name}
                  </Tabs.Tab>
                ))}
              </Tabs>
            </Flex.Item>
            <Flex.Item grow={1} basis={0}>
              <CraftingList craftables={shownRecipes} />
            </Flex.Item>
          </Flex>
        </Section>
      </Window.Content>
    </Window>
  );
};

const CraftingList = (props, context) => {
  const {
    craftables = [],
  } = props;
  const { act, data } = useBackend(context);
  const {
    craftability = {},
    display_compact,
    display_craftable_only,
  } = data;
  return craftables.map(craftable => {
    if (display_craftable_only && !craftability[craftable.ref]) {
      return null;
    }
    // Compact display
    if (display_compact) {
      return (
        <LabeledList.Item
          key={craftable.name}
          label={craftable.name}
          className="candystripe"
          buttons={(
            <Button
              icon="cog"
              content="Craft"
              disabled={!craftability[craftable.ref]}
              tooltip={craftable.tool_text && (
                'Tools needed: ' + craftable.tool_text
              )}
              tooltipPosition="left"
              onClick={() => act('make', {
                recipe: craftable.ref,
              })} />
          )}>
          {craftable.req_text}
        </LabeledList.Item>
      );
    }
    // Full display
    return (
      <Section
        key={craftable.name}
        title={craftable.name}
        level={2}
        buttons={(
          <Button
            icon="cog"
            content="Craft"
            disabled={!craftability[craftable.ref]}
            onClick={() => act('make', {
              recipe: craftable.ref,
            })} />
        )}>
        <LabeledList>
          {!!craftable.req_text && (
            <LabeledList.Item label="Required">
              {craftable.req_text}
            </LabeledList.Item>
          )}
          {!!craftable.catalyst_text && (
            <LabeledList.Item label="Catalyst">
              {craftable.catalyst_text}
            </LabeledList.Item>
          )}
          {!!craftable.tool_text && (
            <LabeledList.Item label="Tools">
              {craftable.tool_text}
            </LabeledList.Item>
          )}
        </LabeledList>
      </Section>
    );
  });
};


code:
import { ProgressBar, NumberInput, Button, Section, Box, Flex } from '../components';
import { useBackend } from '../backend';
import { Window } from '../layouts';

export const Photocopier = (props, context) => {
  const { data } = useBackend(context);
  const {
    isAI,
    has_toner,
    has_item,
  } = data;

  return (
    <Window
      title="Photocopier"
      width={240}
      height={isAI ? 309 : 234}>
      <Window.Content>
        {has_toner ? (
          <Toner />
        ) : (

            <Box color="average">
              No inserted toner cartridge.
            </Box>

        )}
        {has_item ? (
          <Options />
        ) : (

            <Box color="average">
              No inserted item.
            </Box>

        )}
        {!!isAI && (
          <AIOptions />
        )}
      </Window.Content>
    </Window>
  );
};

const Toner = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    has_toner,
    max_toner,
    current_toner,
  } = data;

  const average_toner = max_toner * 0.66;
  const bad_toner = max_toner * 0.33;

  return (
    <Section
      title="Toner"
      buttons={
        <Button
          disabled={!has_toner}
          onClick={() => act('remove_toner')}
          icon="eject">
          Eject
        </Button>
      }>
      <ProgressBar
        ranges={{
          good: [average_toner, max_toner],
          average: [bad_toner, average_toner],
          bad: [0, bad_toner],
        }}
        value={current_toner}
        minValue={0}
        maxValue={max_toner} />

  );
};

const Options = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    color_mode,
    is_photo,
    num_copies,
    has_enough_toner,
  } = data;

  return (

      <Flex>
        <Flex.Item
          mt={0.4}
          width={11}
          color="label">
          Make copies:
        </Flex.Item>
        <Flex.Item>
          <NumberInput
            animate
            width={2.6}
            height={1.65}
            step={1}
            stepPixelSize={8}
            minValue={1}
            maxValue={10}
            value={num_copies}
            onDrag={(e, value) => act('set_copies', {
              num_copies: value,
            })} />
        </Flex.Item>
        <Flex.Item>
          <Button
            ml={0.2}
            icon="copy"
            textAlign="center"
            disabled={!has_enough_toner}
            onClick={() => act('make_copy')}>
            Copy
          </Button>
        </Flex.Item>
      </Flex>
      {!!is_photo && (
        <Flex mt={0.5}>
          <Flex.Item
            mr={0.4}
            width="50%">
            <Button
              fluid
              textAlign="center"
              selected={color_mode === "Greyscale"}
              onClick={() => act('color_mode', {
                mode: "Greyscale",
              })}>
              Greyscale
            </Button>
          </Flex.Item>
          <Flex.Item
            ml={0.4}
            width="50%">
            <Button
              fluid
              textAlign="center"
              selected={color_mode === "Color"}
              onClick={() => act('color_mode', {
                mode: "Color",
              })}>
              Color
            </Button>
          </Flex.Item>
        </Flex>
      )}
      <Button
        mt={0.5}
        textAlign="center"
        icon="reply"
        fluid
        onClick={() => act('remove')}>
        Remove item
      </Button>

  );
};

const AIOptions = (props, context) => {
  const { act, data } = useBackend(context);
  const { can_AI_print } = data;

  return (

      <Box>
        <Button
          fluid
          icon="images"
          textAlign="center"
          disabled={!can_AI_print}
          onClick={() => act('ai_photo')}>
          Print photo from database
        </Button>
      </Box>

  );
};