├── packages ├── data │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── scripts │ │ │ ├── ignored-properties.ts │ │ │ ├── download-assets.ts │ │ │ ├── reparse.ts │ │ │ └── ignored-types.ts │ │ └── types │ │ │ └── dump.ts │ ├── tsconfig.json │ └── package.json ├── ui │ ├── README.md │ ├── .gitignore │ ├── src │ │ ├── ìnput.css │ │ ├── hooks │ │ │ ├── use-entry.ts │ │ │ ├── use-entries-of-type.ts │ │ │ └── use-resolve-joint-item-entries.ts │ │ ├── parts │ │ │ ├── locale-name.tsx │ │ │ ├── locale-description.tsx │ │ │ ├── ingredient.tsx │ │ │ ├── technology-entity-button.stories.tsx │ │ │ ├── group-tabs.tsx │ │ │ ├── entity-grid.tsx │ │ │ ├── data-provider.tsx │ │ │ ├── factorio-image.tsx │ │ │ ├── entity-button.tsx │ │ │ ├── technology-entity-button.tsx │ │ │ ├── entity-tooltip.tsx │ │ │ └── entity-sections.tsx │ │ ├── components │ │ │ ├── tooltip-section.tsx │ │ │ ├── tooltip-root.tsx │ │ │ ├── technology-button.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── table-cell.tsx │ │ │ ├── content-section.tsx │ │ │ ├── tabs.tsx │ │ │ ├── quality-tiering.tsx │ │ │ ├── button-grid.tsx │ │ │ ├── surface.tsx │ │ │ └── table.tsx │ │ ├── utils.ts │ │ ├── stories │ │ │ ├── entity-button.stories.tsx │ │ │ ├── entity-sections.stories.tsx │ │ │ ├── ingredient.stories.tsx │ │ │ ├── group-tabs.stories.tsx │ │ │ ├── technology-button.stories.tsx │ │ │ ├── tabs.stories.tsx │ │ │ ├── quality-tiering.stories.tsx │ │ │ ├── 1-hooks.mdx │ │ │ ├── tooltip.stories.tsx │ │ │ ├── entity-tooltip.stories.tsx │ │ │ ├── content-section.stories.tsx │ │ │ ├── data-hooks.stories.tsx │ │ │ ├── table.stories.tsx │ │ │ ├── 0-intro.mdx │ │ │ ├── button-grid.stories.tsx │ │ │ ├── entity-grid.stories.tsx │ │ │ └── surface.stories.tsx │ │ ├── index.ts │ │ └── output.css │ ├── tsconfig.json │ ├── .storybook │ │ ├── preview.tsx │ │ └── main.ts │ ├── tailwind.config.js │ └── package.json └── pedia │ ├── src │ ├── vite-env.d.ts │ ├── tools │ │ ├── index.ts │ │ ├── dummy.tsx │ │ ├── solarratio.tsx │ │ ├── heatlist.tsx │ │ ├── fuels.tsx │ │ ├── unithealth.tsx │ │ └── fusionratio.tsx │ ├── urls.ts │ ├── routes │ │ ├── index.lazy.tsx │ │ ├── tool.$tool.tsx │ │ ├── about.lazy.tsx │ │ ├── technology.$name.tsx │ │ ├── pedia.$type.$name.tsx │ │ └── __root.tsx │ ├── components │ │ ├── mobile-menu-provider.tsx │ │ ├── tabbed-content-pane.tsx │ │ └── two-column-container.tsx │ ├── App.tsx │ ├── index.css │ └── main.tsx │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── .gitignore │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── package.json │ └── public │ └── vite.svg ├── .yarnrc.yml ├── lerna.json ├── tsconfig.lint.json ├── homepagedata.json ├── ideas.md ├── .gitignore ├── .github └── workflows │ ├── verify.yml │ └── publish.yml ├── tsconfig.json ├── package.json └── README.md /packages/data/.gitignore: -------------------------------------------------------------------------------- 1 | data -------------------------------------------------------------------------------- /packages/data/README.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | src/output.css 2 | storybook-static -------------------------------------------------------------------------------- /packages/data/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types/dump"; 2 | -------------------------------------------------------------------------------- /packages/pedia/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/ui/src/ìnput.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /packages/data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../tsconfig.json"], 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../tsconfig.json"], 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "0.0.3" 4 | } -------------------------------------------------------------------------------- /tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["packages"], 4 | "exclude": [] 5 | } -------------------------------------------------------------------------------- /homepagedata.json: -------------------------------------------------------------------------------- 1 | { 2 | "repo": "factoriopedia", 3 | "title": "Factoriopedia", 4 | "category": "app" 5 | } 6 | -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | - Tools 2 | - Solar Ratio Calculator 3 | - Nuclear Ratio Calculator 4 | - Fusion Ratio Calculator 5 | - Technology Viewer -------------------------------------------------------------------------------- /packages/pedia/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/pedia/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import config from "../ui/tailwind.config.js"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/pedia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./heatlist"; 2 | export * from "./unithealth"; 3 | // export * from "./solarratio"; 4 | export * from "./fuels"; 5 | export * from "./fusionratio"; 6 | -------------------------------------------------------------------------------- /packages/pedia/src/urls.ts: -------------------------------------------------------------------------------- 1 | export const urls = { 2 | homepage: "https://lukasbach.com", 3 | storybook: "https://factoriopedia.lukasbach.com/storybook", 4 | repo: "https://github.com/lukasbach/factoriopedia", 5 | ghuser: "https://github.com/lukasbach", 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .pnp.* 3 | .yarn/* 4 | !.yarn/patches 5 | !.yarn/plugins 6 | !.yarn/releases 7 | !.yarn/sdks 8 | !.yarn/versions 9 | node_modules 10 | yarn-error.log 11 | 12 | lib 13 | 14 | packages/data/script-output 15 | *storybook.log 16 | .iml 17 | .idea -------------------------------------------------------------------------------- /packages/ui/src/hooks/use-entry.ts: -------------------------------------------------------------------------------- 1 | import { useFactorioData } from "../parts/data-provider"; 2 | 3 | export const useEntry = (name: string, type?: string) => { 4 | const data = useFactorioData(); 5 | const entry = data.entries[name]; 6 | return !type || entry[type] ? entry : undefined; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/dummy.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Tool } from "../routes/tool.$tool"; 3 | 4 | const ToolRender: FC = () => { 5 | return
Hello
; 6 | }; 7 | 8 | export const dummyTool: Omit = { 9 | title: "Dummmy tool", 10 | icon: ["item", "heating-tower"], 11 | render: ToolRender, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/solarratio.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Tool } from "../routes/tool.$tool"; 3 | 4 | const ToolRender: FC = () =>
Hello
; 5 | 6 | export const solarratio: Omit = { 7 | title: "Ratio Calculator for Solar Panels", 8 | icon: ["item", "solar-panel"], 9 | render: ToolRender, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/parts/locale-name.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useFactorioData } from "./data-provider"; 3 | 4 | export const useLocaleName = (name: string) => 5 | useFactorioData().locales.names[name]?.replace(/\[entity=[^\]]+\]/, "") ?? 6 | name; 7 | 8 | export const LocaleName: FC<{ 9 | name: string; 10 | }> = ({ name }) => <>{useLocaleName(name)}; 11 | -------------------------------------------------------------------------------- /packages/pedia/src/routes/index.lazy.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, createLazyFileRoute } from "@tanstack/react-router"; 2 | 3 | const Page = () => { 4 | return ( 5 | 9 | ); 10 | }; 11 | 12 | export const Route = createLazyFileRoute("/")({ 13 | component: Page, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/ui/src/components/tooltip-section.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | 3 | export const TooltipSection: FC> = ({ 4 | topmost, 5 | children, 6 | }) => { 7 | return ( 8 |
11 | {children} 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/pedia/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | src/routeTree.gen.ts -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: volta-cli/action@v4 15 | - run: yarn install 16 | - run: yarn download 17 | - run: yarn build 18 | - run: yarn lint 19 | -------------------------------------------------------------------------------- /packages/pedia/src/components/mobile-menu-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | const MobileMenuProviderContext = createContext<{ 4 | isMenuOpen: boolean; 5 | openMenu: () => void; 6 | closeMenu: () => void; 7 | }>(null as any); 8 | 9 | export const MobileMenuProvider = MobileMenuProviderContext.Provider; 10 | export const useMobileMenu = () => useContext(MobileMenuProviderContext); 11 | -------------------------------------------------------------------------------- /packages/pedia/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Factoriopedia 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/ui/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const combine = ( 2 | ...styles: (string | [boolean | undefined, ...string[]] | string[])[] 3 | ) => 4 | styles 5 | .filter((style) => !!style[0]) 6 | .map((style) => 7 | // eslint-disable-next-line no-nested-ternary 8 | typeof style === "string" 9 | ? [style] 10 | : typeof style[0] === "string" 11 | ? style.slice(1) 12 | : style, 13 | ) 14 | .flat() 15 | .join(" "); 16 | -------------------------------------------------------------------------------- /packages/pedia/vite.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { defineConfig } from "vite"; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import react from "@vitejs/plugin-react-swc"; 5 | import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig({ 9 | publicDir: "../data/data", 10 | plugins: [react(), TanStackRouterVite()], 11 | }); 12 | -------------------------------------------------------------------------------- /packages/ui/src/parts/locale-description.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useFactorioData } from "./data-provider"; 3 | 4 | export const useLocaleDescription = (name: string) => 5 | useFactorioData().locales.descriptions?.[name]; 6 | 7 | export const LocaleDescription: FC<{ 8 | name: string; 9 | }> = ({ name }) => ( 10 | // eslint-disable-next-line react/jsx-no-useless-fragment 11 | <>{useFactorioData().locales.descriptions?.[name] ?? "No description"} 12 | ); 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "Bundler", 6 | "target": "ES2017", 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "lib": [ 10 | "DOM" 11 | ], 12 | "outDir": "${configDir}/lib", 13 | "strict": true, 14 | "declaration": true, 15 | "jsx": "react-jsx", 16 | "noImplicitAny": false 17 | }, 18 | "include": ["packages"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/src/stories/entity-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { EntityButton } from "../parts/entity-button"; 3 | import { TooltipRoot } from "../components/tooltip-root"; 4 | 5 | const meta = { 6 | title: "Components/Entity Button", 7 | component: EntityButton, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const Sample = () => ( 13 | <> 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/use-entries-of-type.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { DumpType } from "@factorioui/data"; 3 | import { useFactorioData } from "../parts/data-provider"; 4 | 5 | export const useEntriesOfType = ( 6 | type: string, 7 | filter?: (entry: DumpType["entries"][string]) => boolean, 8 | ) => { 9 | const { entries, typeMap } = useFactorioData(); 10 | return useMemo( 11 | () => 12 | typeMap[type].map((name) => entries[name]).filter(filter ?? (() => true)), 13 | [entries, filter, type, typeMap], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ui/src/stories/entity-sections.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { EntitySection } from "../parts/entity-sections"; 3 | 4 | const meta = { 5 | title: "Parts/Entity Sections", 6 | } satisfies Meta; 7 | 8 | export default meta; 9 | 10 | export const ItemEntitySections = () => { 11 | return ( 12 |
13 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/tooltip-root.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as ReactTooltip } from "react-tooltip"; 2 | 3 | function isTouchDevice() { 4 | return ( 5 | "ontouchstart" in window || 6 | (navigator as any).maxTouchPoints > 0 || 7 | (navigator as any).msMaxTouchPoints > 0 8 | ); 9 | } 10 | 11 | export const TooltipRoot = () => 12 | isTouchDevice() ? null : ( 13 | 22 | ); 23 | -------------------------------------------------------------------------------- /packages/ui/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | decorators: [(Story) => ( 13 | Loading...}> 14 | 15 | 16 | )], 17 | }; 18 | 19 | import "../src/output.css"; 20 | import {FactorioDataProvider} from "../src/parts/data-provider"; 21 | document.body.style.background = "#242324"; 22 | 23 | export default preview; 24 | -------------------------------------------------------------------------------- /packages/data/src/scripts/ignored-properties.ts: -------------------------------------------------------------------------------- 1 | export const ignoredProperties = [ 2 | /noise/, 3 | /^place_every_n$/, 4 | /^quick_multioctave_noise_persistence$/, 5 | /^resource_autoplace_all_patches$/, 6 | /^circuit_connector$/, 7 | /sound$/, 8 | /sounds$/, 9 | /^colors$/, 10 | /^factoriopedia_simulation$/, 11 | /_box$/, 12 | /^hatch_definitions$/, 13 | /^variants$/, 14 | /^variations$/, 15 | /^variation_weights$/, 16 | /graphics_set$/, 17 | /animation$/, 18 | /_tint$/, 19 | /.*(? { 4 | return ( 5 | 9 | 14 | 15 | Left 16 | 17 | 18 | Right 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/pedia/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/src/parts/ingredient.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { EntityButton } from "./entity-button"; 3 | import { LocaleName } from "./locale-name"; 4 | 5 | export const Ingredient: FC<{ 6 | name: string; 7 | count: number | string; 8 | label?: string; 9 | type?: string; 10 | }> = ({ count, name, label, type }) => { 11 | return ( 12 |
13 | 14 |
15 | 16 | {count} 17 | {!label ? " x" : ""} 18 | {" "} 19 | {label ?? } 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/ui/src/stories/ingredient.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { Surface } from "../components/surface"; 3 | import { Ingredient } from "../parts/ingredient"; 4 | 5 | const meta = { 6 | title: "Parts/Ingredient", 7 | component: Ingredient, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const Ingredients = () => ( 13 | 14 | 15 | 16 | 17 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /packages/pedia/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | pages: write 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: volta-cli/action@v4 18 | - run: yarn install 19 | - run: yarn download 20 | - run: yarn build 21 | - run: yarn lint 22 | - run: mv packages/ui/storybook-static packages/pedia/dist/storybook 23 | - uses: actions/upload-pages-artifact@v3 24 | id: artifact 25 | with: 26 | path: packages/pedia/dist 27 | - uses: actions/deploy-pages@v4 28 | with: 29 | artifact_name: github-pages 30 | -------------------------------------------------------------------------------- /packages/pedia/src/components/tabbed-content-pane.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, ReactNode } from "react"; 2 | import { Surface } from "@factorioui/components"; 3 | 4 | export const TabbedContentPane: FC< 5 | PropsWithChildren<{ title: ReactNode; tabsList: ReactNode }> 6 | > = ({ tabsList, title, children }) => { 7 | return ( 8 |
9 | 10 |
11 | {title} 12 |
13 | {tabsList} 14 |
15 | 20 | {children} 21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/pedia/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | line-height: 1.5; 8 | font-weight: 400; 9 | 10 | color-scheme: light dark; 11 | color: rgba(255, 255, 255, 0.87); 12 | background-color: #242424; 13 | 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | ::-webkit-scrollbar { 21 | width: 4px; 22 | height: 4px; 23 | } 24 | 25 | ::-webkit-scrollbar-track { 26 | background: transparent; 27 | } 28 | 29 | ::-webkit-scrollbar-thumb { 30 | background: #828282; 31 | } 32 | 33 | ::-webkit-scrollbar-thumb:hover { 34 | background: #d48e2d; 35 | } -------------------------------------------------------------------------------- /packages/ui/src/stories/group-tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import React, { useState } from "react"; 3 | import { GroupTabs } from "../parts/group-tabs"; 4 | import { Surface } from "../components/surface"; 5 | 6 | const meta = { 7 | title: "Parts/Group Tabs", 8 | component: GroupTabs, 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | 13 | export const GroupTabsExample = () => { 14 | const [selectedGroup, setSelectedGroup] = useState( 15 | "intermediate-products", 16 | ); 17 | 18 | return ( 19 | 20 | 25 | {selectedGroup} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/pedia/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { 4 | RouterProvider, 5 | createHashHistory, 6 | createRouter, 7 | } from "@tanstack/react-router"; 8 | import { routeTree } from "./routeTree.gen"; 9 | 10 | import "./index.css"; 11 | import "@factorioui/components/src/output.css"; 12 | 13 | const router = createRouter({ routeTree, history: createHashHistory() }); 14 | 15 | declare module "@tanstack/react-router" { 16 | interface Register { 17 | router: typeof router; 18 | } 19 | } 20 | 21 | // Render the app 22 | const rootElement = document.getElementById("root")!; 23 | if (!rootElement.innerHTML) { 24 | const root = createRoot(rootElement); 25 | root.render( 26 | 27 | 28 | , 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/data/src/types/dump.ts: -------------------------------------------------------------------------------- 1 | export type FactorioType = { 2 | type: string; 3 | name: string; 4 | hidden: boolean; 5 | order: string; 6 | subgroup: string; 7 | [key: string]: any; 8 | }; 9 | 10 | export type DumpType = { 11 | entries: Record< 12 | string, 13 | { 14 | merged: FactorioType; 15 | types: string[]; 16 | } & { [type: string]: FactorioType } 17 | >; 18 | typeMap: Record; 19 | subgroupMap: Record; 20 | groupMap: Record; 21 | locales: { 22 | names: Record; 23 | descriptions: Record; 24 | }; 25 | spriteMap: Record< 26 | string, 27 | Record 28 | >; 29 | spriteMapSizes: Record; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ui/src/parts/technology-entity-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { TechnologyEntityButton } from "./technology-entity-button"; 3 | import { TooltipRoot } from "../components/tooltip-root"; 4 | 5 | const meta = { 6 | title: "Parts/Technology Entity Button", 7 | component: TechnologyEntityButton, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const BasicTechnologyButton = () => ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | -------------------------------------------------------------------------------- /packages/pedia/src/components/two-column-container.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from "react"; 2 | import { Surface } from "@factorioui/components"; 3 | import { useMobileMenu } from "./mobile-menu-provider"; 4 | 5 | export const TwoColumnContainer: FC<{ left: ReactNode; right: ReactNode }> = ({ 6 | left, 7 | right, 8 | }) => { 9 | const { isMenuOpen } = useMobileMenu(); 10 | return ( 11 | <> 12 | 17 | {left} 18 | 19 | 24 | {right} 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ui/src/components/technology-button.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, ReactNode } from "react"; 2 | 3 | export const TechnologyButton: FC< 4 | PropsWithChildren<{ 5 | subtext?: ReactNode; 6 | right?: string; 7 | onClick?: () => void; 8 | }> 9 | > = ({ children, right, subtext, onClick }) => { 10 | return ( 11 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, createContext, useContext } from "react"; 2 | import ReactDOMServer from "react-dom/server"; 3 | 4 | const IsInTooltipContext = createContext(false); 5 | export const useIsInTooltip = () => useContext(IsInTooltipContext) || false; 6 | 7 | export const tooltip = ( 8 | text: string | ReactNode | null, 9 | children?: ReactNode, 10 | ) => { 11 | return { 12 | "data-tooltip-id": "factorio-ui-tooltip", 13 | "data-tooltip-html": ReactDOMServer.renderToStaticMarkup( 14 | 15 |
16 | {text && ( 17 |
18 | {text} 19 |
20 | )} 21 | {children} 22 |
23 |
, 24 | ), 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/ui/src/stories/technology-button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { TechnologyButton } from "../components/technology-button"; 3 | import { FactorioImage } from "../parts/factorio-image"; 4 | 5 | const meta = { 6 | title: "Components/Technology Button", 7 | component: TechnologyButton, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | 12 | export const BasicTechnologyButton = () => ( 13 | 17 | 18 | 19 | 20 | 21 | } 22 | > 23 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /packages/ui/src/stories/tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { Surface } from "../components/surface"; 3 | import { 4 | TabsContent, 5 | TabsList, 6 | TabsRoot, 7 | TabsTrigger, 8 | } from "../components/tabs"; 9 | 10 | const meta = { 11 | title: "Components/Tabs", 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | 16 | export const BasicTabs = () => ( 17 | 18 | 19 | 20 | Manage 21 | Install 22 | Updates 23 | 24 | 25 | 26 | Manage 27 | Install 28 | Updates 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/button-grid"; 2 | export * from "./components/surface"; 3 | export * from "./components/tooltip"; 4 | export * from "./components/tooltip-section"; 5 | export * from "./components/tooltip-root"; 6 | export * from "./components/tabs"; 7 | export * from "./components/table"; 8 | export * from "./components/content-section"; 9 | export * from "./components/technology-button"; 10 | 11 | export * from "./parts/entity-button"; 12 | export * from "./parts/entity-grid"; 13 | export * from "./parts/entity-tooltip"; 14 | export * from "./parts/group-tabs"; 15 | export * from "./parts/locale-name"; 16 | export * from "./parts/locale-description"; 17 | export * from "./parts/entity-sections"; 18 | export * from "./parts/ingredient"; 19 | export * from "./parts/technology-entity-button"; 20 | export * from "./parts/data-provider"; 21 | export * from "./parts/factorio-image"; 22 | 23 | export * from "./hooks/use-entries-of-type"; 24 | export * from "./hooks/use-resolve-joint-item-entries"; 25 | -------------------------------------------------------------------------------- /packages/pedia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@factorioui/pedia", 3 | "private": true, 4 | "version": "0.0.3", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build && tsc -b", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@factorioui/components": "workspace:^", 13 | "@factorioui/data": "workspace:^", 14 | "@tanstack/react-router": "^1.87.7", 15 | "@tanstack/react-table": "^8.20.6", 16 | "autoprefixer": "^10.4.20", 17 | "postcss": "^8.4.49", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "tailwindcss": "^3.4.16", 21 | "zod": "^3.24.1" 22 | }, 23 | "devDependencies": { 24 | "@tanstack/router-devtools": "^1.87.7", 25 | "@tanstack/router-plugin": "^1.87.7", 26 | "@types/react": "^18.3.12", 27 | "@types/react-dom": "^18.3.1", 28 | "@vitejs/plugin-react-swc": "^3.5.0", 29 | "globals": "^15.12.0", 30 | "typescript": "~5.6.2", 31 | "typescript-eslint": "^8.15.0", 32 | "vite": "^6.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ui/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | import { join, dirname } from "path"; 4 | 5 | /** 6 | * This function is used to resolve the absolute path of a package. 7 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 8 | */ 9 | function getAbsolutePath(value: string): any { 10 | return dirname(require.resolve(join(value, "package.json"))); 11 | } 12 | const config: StorybookConfig = { 13 | stories: [ 14 | "../src/**/*.mdx", 15 | "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)", 16 | ], 17 | addons: [ 18 | getAbsolutePath("@storybook/addon-onboarding"), 19 | getAbsolutePath("@storybook/addon-essentials"), 20 | getAbsolutePath("@chromatic-com/storybook"), 21 | getAbsolutePath("@storybook/addon-interactions"), 22 | ], 23 | staticDirs: [ 24 | // getAbsolutePath("@factorioui/data/data") 25 | "../../data/data", 26 | ], 27 | framework: { 28 | name: getAbsolutePath("@storybook/react-vite"), 29 | options: {}, 30 | }, 31 | }; 32 | export default config; 33 | -------------------------------------------------------------------------------- /packages/data/src/scripts/download-assets.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import tarInstall from "tar-install"; 3 | import * as fs from "fs-extra"; 4 | import path from "node:path"; 5 | import url from "node:url"; 6 | 7 | const targetFolder = path.join( 8 | path.dirname(url.fileURLToPath(import.meta.url)), 9 | "../../data", 10 | ); 11 | const tempFolder = path.join( 12 | path.dirname(url.fileURLToPath(import.meta.url)), 13 | "../../temp", 14 | ); 15 | 16 | await fs.ensureDir(targetFolder); 17 | await fs.ensureDir(tempFolder); 18 | const data = await fetch("https://registry.npmjs.org/@factorioui/data").then( 19 | (res) => res.json(), 20 | ); 21 | const version = data["dist-tags"].latest; 22 | // const { tarball } = data.versions[version].dist; 23 | // const response = await fetch(tarball); 24 | // const buffer = await response.arrayBuffer(); 25 | // const zip = new Uint8Array(buffer); 26 | // await fs.outputFile("data.tgz", zip); 27 | await tarInstall(data.versions[version].dist.tarball, tempFolder); 28 | await fs.move(path.join(tempFolder, `data-${version}`, "data"), targetFolder, { 29 | overwrite: true, 30 | }); 31 | await fs.remove(tempFolder); 32 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/use-resolve-joint-item-entries.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useFactorioData } from "../parts/data-provider"; 3 | 4 | type ResolvementProps = { 5 | group: string; 6 | types?: string[]; 7 | }; 8 | 9 | export const useResolveJointItemEntries = (props: ResolvementProps) => { 10 | const data = useFactorioData(); 11 | return useMemo( 12 | () => 13 | data.groupMap[props.group] 14 | .map((subgroup) => { 15 | return data.subgroupMap[subgroup] 16 | ?.filter( 17 | (entry) => !props.types || props.types.includes(entry.type), 18 | ) 19 | ?.map((entry) => { 20 | return { 21 | entry: data.entries[entry.name], 22 | name: entry.name, 23 | type: entry.type, 24 | }; 25 | }); 26 | // .filter( 27 | // (entry) => 28 | // !props.types || 29 | // entry.types.some((type) => props.types?.includes(type)), 30 | // ) ?? [] 31 | }) 32 | .filter((subgroup) => subgroup && subgroup.length > 0), 33 | [data, props.group, props.types], 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/heatlist.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { 3 | FactorioImage, 4 | LocaleName, 5 | Table, 6 | useEntriesOfType, 7 | } from "@factorioui/components"; 8 | import { createColumnHelper } from "@tanstack/react-table"; 9 | import { Tool } from "../routes/tool.$tool"; 10 | 11 | const columnHelper = createColumnHelper(); 12 | const columns = [ 13 | columnHelper.accessor("merged.name", { 14 | header: () => "", 15 | size: 28, 16 | cell: (info) => ( 17 | 18 | ), 19 | }), 20 | columnHelper.accessor("merged.name", { 21 | header: () => "Item", 22 | cell: (info) => , 23 | }), 24 | columnHelper.accessor("merged.heating_energy", { 25 | header: () => "Heating Energy", 26 | }), 27 | ]; 28 | 29 | const ToolRender: FC = () => { 30 | const data = useEntriesOfType("item", (entry) => entry.merged.heating_energy); 31 | return ; 32 | }; 33 | 34 | export const heatlist: Omit = { 35 | title: "Heating Energy of items", 36 | icon: ["item", "heating-tower"], 37 | render: ToolRender, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ui/src/stories/quality-tiering.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { Surface } from "../components/surface"; 3 | import { 4 | ContentSection, 5 | ContentSectionStat, 6 | } from "../components/content-section"; 7 | import { QualityTiering } from "../components/quality-tiering"; 8 | import { TooltipRoot } from "../components/tooltip-root"; 9 | 10 | const meta = { 11 | title: "Components/Quality Tiering", 12 | component: ContentSection, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | 17 | export const QualityTieringExample = () => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "devDependencies": { 8 | "@lukasbach/eslint-config-deps": "^1.0.7", 9 | "eslint": "8", 10 | "lerna": "^8.1.9", 11 | "typescript": "latest" 12 | }, 13 | "volta": { 14 | "node": "22.12.0", 15 | "yarn": "4.5.3" 16 | }, 17 | "scripts": { 18 | "lint": "eslint .", 19 | "lint:fix": "eslint . --fix", 20 | "build": "lerna run build", 21 | "build:ci": "lerna run build,build-storybook", 22 | "dev": "lerna run dev,build:watch,tailwind:watch,storybook --parallel", 23 | "download": "lerna run download", 24 | "reparse": "lerna run reparse" 25 | }, 26 | "eslintConfig": { 27 | "extends": "@lukasbach/base/react", 28 | "parserOptions": { 29 | "project": "./tsconfig.lint.json" 30 | }, 31 | "rules": { 32 | "react/destructuring-assignment": "off", 33 | "@typescript-eslint/no-use-before-define": "off" 34 | }, 35 | "ignorePatterns": [ 36 | "lib", 37 | "dist", 38 | "*.js" 39 | ] 40 | }, 41 | "type": "module", 42 | "exports": "./lib/index.js", 43 | "typings": "lib/index.d.ts", 44 | "engines": { 45 | "node": ">=22" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/ui/src/parts/group-tabs.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Surface } from "../components/surface"; 3 | import { FactorioImage } from "./factorio-image"; 4 | import { ButtonGrid } from "../components/button-grid"; 5 | import { useEntriesOfType } from "../hooks/use-entries-of-type"; 6 | 7 | export const GroupTabs: FC<{ 8 | gridWidth: number; 9 | selectedGroup: string | undefined; 10 | onSelectGroup: (group: string) => void; 11 | }> = ({ gridWidth, onSelectGroup, selectedGroup }) => { 12 | const groups = useEntriesOfType("item-group"); 13 | return ( 14 | 15 | {groups.map((group) => ( 16 | 17 | as="button" 18 | key={group.merged.name} 19 | shadow="btn-large" 20 | color="grayLight" 21 | hover={{ color: "orangeDark", shadow: "orangeglow" }} 22 | active={{ color: "orangeLight", shadow: "deepinset" }} 23 | className="flex w-[80px] h-[96px] items-center justify-center" 24 | isActive={selectedGroup === group.merged.name} 25 | onClick={() => onSelectGroup(group.merged.name)} 26 | > 27 | 28 | 29 | ))} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/ui/src/stories/1-hooks.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Source, Stories, Story} from '@storybook/blocks'; 2 | import * as DataHooksStories from './data-hooks.stories'; 3 | 4 | 5 | # Data Hooks 6 | 7 | The library exposes a few utility hooks that can be used for interacting with Factorio data. 8 | 9 | # `useFactorioData` 10 | 11 | This hook is used to access the Factorio data dump. It is a wrapper around the `useContext` hook and returns the data dump. 12 | 13 | 14 | 15 | 16 | # `useFactorioDataPath` 17 | 18 | Exposes the path to the Factorio data dump. 19 | 20 | 21 | 22 | 23 | # `useEntriesOfType` 24 | 25 | Returns a list of all entries in the data dump of a certain type. 26 | 27 | 28 | 29 | 30 | # `useEntry` 31 | 32 | Returns a single entry from the data dump. 33 | 34 | 35 | 36 | 37 | # `UseResolvedJointItemEntries` 38 | 39 | Returns a list of all entries in the data dump of a group, divided by subgroups. 40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/pedia/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/components/table-cell.tsx: -------------------------------------------------------------------------------- 1 | import { FC, HTMLAttributes, PropsWithChildren } from "react"; 2 | import { combine } from "../utils"; 3 | import { Surface } from "./surface"; 4 | 5 | export const TableCell: FC< 6 | PropsWithChildren< 7 | { 8 | sorting?: "asc" | "desc"; 9 | onClick?: (e: unknown) => void; 10 | isTitle?: boolean; 11 | isActive?: boolean; 12 | } & HTMLAttributes 13 | > 14 | > = ({ onClick, sorting, isTitle, isActive, children, ...props }) => { 15 | return ( 16 | 17 | as={isTitle ? "th" : "td"} 18 | useGroup 19 | shadow="btn-small" 20 | hover={onClick && { color: "orangeDark", shadow: "btn-large" }} 21 | active={ 22 | onClick || isActive 23 | ? { color: "orangeLight", shadow: "inset-1" } 24 | : undefined 25 | } 26 | isActive={isActive} 27 | className={combine( 28 | "text-white px-2 py-0.5", 29 | [!!onClick, "group-hover:text-black cursor-pointer"], 30 | [isTitle, "font-bold"], 31 | [isActive, "!text-black"], 32 | )} 33 | onClick={onClick} 34 | {...props} 35 | color="blackDark" 36 | > 37 | {sorting && ( 38 | 39 | {sorting === "asc" && "▼"} 40 | {sorting === "desc" && "▲"} 41 | 42 | )} 43 | {children} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ui/src/stories/tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { tooltip } from "../components/tooltip"; 3 | import { Surface } from "../components/surface"; 4 | import { FactorioImage } from "../parts/factorio-image"; 5 | import { TooltipSection } from "../components/tooltip-section"; 6 | import { TooltipRoot } from "../components/tooltip-root"; 7 | 8 | const meta = { 9 | title: "Components/Tooltip", 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | 14 | export const BasicToolTip = () => ( 15 | <> 16 | 25 | 26 | sa jslkdaf jslkafö safjdk öaslf klasdf jlas fdlköa föaklsdkf asdf 27 | sadf 28 | 29 | 30 | sa jslkdaf jslkafö safjdk öaslf klasdf jlas fdlköa föaklsdkf asdf 31 | sadf 32 | 33 | 34 | sa jslkdaf jslkafö safjdk öaslf klasdf jlas fdlköa föaklsdkf asdf 35 | sadf 36 | 37 | , 38 | )} 39 | > 40 | 41 | 42 | 43 | 44 | ); 45 | -------------------------------------------------------------------------------- /packages/ui/src/stories/entity-tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { EntityTooltip } from "../parts/entity-tooltip"; 3 | import { useResolveJointItemEntries } from "../hooks/use-resolve-joint-item-entries"; 4 | import { Surface } from "../components/surface"; 5 | import { FactorioImage } from "../parts/factorio-image"; 6 | import { TooltipRoot } from "../components/tooltip-root"; 7 | 8 | const meta = { 9 | title: "Parts/Entity Tooltip", 10 | component: EntityTooltip, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | export const ItemsTooltips = () => ( 16 |
17 | 18 | {Object.values( 19 | useResolveJointItemEntries({ 20 | group: "space", 21 | }), 22 | ).map((subgroup) => ( 23 |
24 | {subgroup.map(({ entry }) => ( 25 | 26 | 34 | 35 | 36 | 37 | ))} 38 |
39 | ))} 40 |
41 | ); 42 | -------------------------------------------------------------------------------- /packages/ui/src/parts/entity-grid.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { ButtonGrid } from "../components/button-grid"; 3 | import { EntityButton } from "./entity-button"; 4 | 5 | export type EntityIdentifier = { name: string; type: string }; 6 | 7 | export const EntityGrid: FC<{ 8 | items: EntityIdentifier[][]; 9 | subtexts?: string[][]; 10 | activeItem?: EntityIdentifier; 11 | onClick?: (item: EntityIdentifier) => void; 12 | gridWidth?: number; 13 | gridHeight?: number; 14 | }> = ({ items, activeItem, onClick, gridHeight, gridWidth, subtexts }) => { 15 | return ( 16 | 22 | {items.map((group, idx) => ( 23 | // eslint-disable-next-line react/no-array-index-key 24 |
25 | {group.map((item, idy) => ( 26 |
27 | onClick?.(item) : undefined} 31 | isActive={ 32 | activeItem && 33 | activeItem.name === item.name && 34 | activeItem.type === item.type 35 | } 36 | subtext={subtexts?.[idx]?.[idy]} 37 | /> 38 |
39 | ))} 40 |
41 | ))} 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/fuels.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { 3 | FactorioImage, 4 | LocaleName, 5 | Table, 6 | useEntriesOfType, 7 | } from "@factorioui/components"; 8 | import { createColumnHelper } from "@tanstack/react-table"; 9 | import { Tool } from "../routes/tool.$tool"; 10 | 11 | const columnHelper = createColumnHelper(); 12 | const columns = [ 13 | columnHelper.accessor("merged.name", { 14 | header: () => "", 15 | size: 28, 16 | cell: (info) => ( 17 | 18 | ), 19 | }), 20 | columnHelper.accessor("merged.name", { 21 | header: () => "Item", 22 | cell: (info) => , 23 | }), 24 | // columnHelper.accessor("merged.fuel_category", { 25 | // header: () => "Fuel Category", 26 | // }), 27 | columnHelper.accessor("merged.fuel_value", { 28 | header: () => "Fuel Value", 29 | }), 30 | columnHelper.accessor("merged.fuel_acceleration_multiplier", { 31 | header: () => "Acc.", 32 | cell: (info) => (info.getValue() ? `x${info.getValue()}` : "-"), 33 | }), 34 | columnHelper.accessor("merged.fuel_top_speed_multiplier", { 35 | header: () => "Top Speed", 36 | cell: (info) => (info.getValue() ? `x${info.getValue()}` : "-"), 37 | }), 38 | ]; 39 | 40 | const ToolRender: FC = () => { 41 | const data = useEntriesOfType("item", (entry) => entry.merged.fuel_value); 42 | return
; 43 | }; 44 | 45 | export const fuels: Omit = { 46 | title: "Fuel Values", 47 | icon: ["item", "rocket-fuel"], 48 | render: ToolRender, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/ui/src/stories/content-section.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { Surface } from "../components/surface"; 3 | import { 4 | ContentSection, 5 | ContentSectionStat, 6 | } from "../components/content-section"; 7 | import { EntityButton } from "../parts/entity-button"; 8 | 9 | const meta = { 10 | title: "Components/Content Section", 11 | component: ContentSection, 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | 16 | export const Sections = () => ( 17 | 18 | Content 19 | Content 20 | Content 21 | 22 | ); 23 | 24 | export const Variants = () => ( 25 | 26 | Dark Block 27 | Dark Block 28 | 29 | Dark Block 30 | 31 | Dark Block 32 | 33 | ); 34 | 35 | export const Stats = () => ( 36 | 37 | 38 | Value 39 | Value 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | -------------------------------------------------------------------------------- /packages/data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@factorioui/data", 3 | "type": "module", 4 | "version": "0.0.3", 5 | "description": "Data Dump of Factorio Space Age, reusable for Factorio Fan Projects", 6 | "author": "Lukas Bach ", 7 | "homepage": "", 8 | "license": "MIT", 9 | "main": "lib/index.js", 10 | "types": "lib/index.d.ts", 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib", 17 | "data" 18 | ], 19 | "scripts": { 20 | "build": "tsc", 21 | "reparse": "tsx ./src/scripts/reparse.ts", 22 | "prepack": "yarn build", 23 | "download": "tsx ./src/scripts/download-assets.ts" 24 | }, 25 | "dependencies": { 26 | "zod": "^3.23.8" 27 | }, 28 | "devDependencies": { 29 | "@types/cross-zip": "^4.0.2", 30 | "@types/deepmerge": "^2.2.3", 31 | "@types/fs-extra": "^11.0.4", 32 | "@types/node": "^22.10.1", 33 | "@types/spritesmith": "^3.4.5", 34 | "cross-zip": "^4.0.1", 35 | "deepmerge": "^4.3.1", 36 | "fs-extra": "^11.2.0", 37 | "glob": "^11.0.0", 38 | "spritesmith": "^3.5.1", 39 | "tar-install": "^0.2.2", 40 | "tsx": "^4.19.2", 41 | "typescript": "latest" 42 | }, 43 | "volta": { 44 | "node": "22.12.0" 45 | }, 46 | "funding": "https://github.com/sponsors/lukasbach", 47 | "keywords": [ 48 | "factorio", 49 | "game", 50 | "tool", 51 | "util", 52 | "data", 53 | "dump", 54 | "export", 55 | "spaceage" 56 | ], 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/lukasbach/factoriopedia.git", 60 | "directory": "packages/data" 61 | }, 62 | "publishConfig": { 63 | "access": "public", 64 | "registry": "https://registry.npmjs.org/" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/ui/src/parts/data-provider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FC, 3 | PropsWithChildren, 4 | ReactNode, 5 | createContext, 6 | useContext, 7 | useEffect, 8 | useMemo, 9 | useState, 10 | } from "react"; 11 | import { DumpType } from "@factorioui/data"; 12 | import deepmerge from "deepmerge"; 13 | 14 | const Context = createContext<{ dump: DumpType; path: string }>(null as any); 15 | 16 | export const useFactorioData = () => useContext(Context).dump; 17 | export const useFactorioDataPath = () => useContext(Context).path; 18 | 19 | export const FactorioDataProvider: FC< 20 | PropsWithChildren<{ path: string; loader?: ReactNode }> 21 | > = ({ children, path, loader }) => { 22 | const [data, setData] = useState(null); 23 | 24 | useEffect(() => { 25 | fetch(`${path}${path === "/" ? "" : "/"}data.json`) 26 | .then((res) => res.json() as Promise) 27 | .then((dump) => { 28 | for (const [key, value] of Object.entries(dump.entries)) { 29 | const { types, ...rest } = value; 30 | // eslint-disable-next-line no-param-reassign 31 | dump.entries[key].merged = deepmerge.all(Object.values(rest)) as any; 32 | } 33 | setData(dump); 34 | }); 35 | }, [path]); 36 | const value = useMemo( 37 | () => (data ? { dump: data, path } : undefined), 38 | [data, path], 39 | ); 40 | 41 | if (!value) { 42 | return loader; 43 | } 44 | 45 | return {children}; 46 | }; 47 | 48 | export const StaticFactorioDataProvider: FC< 49 | PropsWithChildren<{ data: DumpType; path: string }> 50 | > = ({ children, data, path }) => ( 51 | ({ dump: data, path }), [data, path])}> 52 | {children} 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/unithealth.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import { 3 | FactorioImage, 4 | LocaleName, 5 | Table, 6 | useEntriesOfType, 7 | } from "@factorioui/components"; 8 | import { createColumnHelper } from "@tanstack/react-table"; 9 | import { Tool } from "../routes/tool.$tool"; 10 | 11 | const columnHelper = createColumnHelper(); 12 | const columns = [ 13 | columnHelper.accessor("merged.name", { 14 | header: () => "", 15 | size: 48, 16 | cell: (info) => ( 17 | 18 | ), 19 | }), 20 | columnHelper.accessor("merged.name", { 21 | header: () => "Unit", 22 | size: 150, 23 | cell: (info) => , 24 | }), 25 | columnHelper.accessor("merged.max_health", { 26 | header: () => "Max Health", 27 | cell: (info) => Math.floor(Number.parseFloat(info.getValue()) * 100) / 100, 28 | }), 29 | columnHelper.accessor("merged.healing_per_tick", { 30 | header: () => "Healing", 31 | cell: (info) => 32 | `${Math.floor(Number.parseFloat(info.getValue()) * 100 * 60) / 100}/s`, 33 | }), 34 | ]; 35 | 36 | const ToolRender: FC = () => { 37 | const units = useEntriesOfType("unit"); 38 | const spiderUnits = useEntriesOfType("spider-unit"); 39 | const segmentedUnits = useEntriesOfType("segmented-unit"); 40 | const turrets = useEntriesOfType("turret"); 41 | const data = useMemo( 42 | () => [...units, ...spiderUnits, ...segmentedUnits, ...turrets], 43 | [units, spiderUnits, segmentedUnits, turrets], 44 | ); 45 | return
; 46 | }; 47 | 48 | export const unithealth: Omit = { 49 | title: "Unit Healths", 50 | icon: ["unit", "small-biter"], 51 | render: ToolRender, 52 | }; 53 | -------------------------------------------------------------------------------- /packages/ui/src/parts/factorio-image.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import { useFactorioData, useFactorioDataPath } from "./data-provider"; 3 | 4 | export const FactorioImage: FC<{ 5 | image: string; 6 | spritesheet?: string; 7 | width?: number; 8 | }> = ({ image, width, spritesheet }) => { 9 | const dataPath = useFactorioDataPath(); 10 | const data = useFactorioData(); 11 | 12 | let resolvedSpritesheet = useMemo(() => { 13 | if (spritesheet && data.spriteMap[spritesheet]?.[`${image}.png`]) 14 | return spritesheet; 15 | return Object.entries(data.spriteMap).find( 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | ([_, images]) => `${image}.png` in images, 18 | )?.[0]; 19 | }, [data.spriteMap, image, spritesheet]); 20 | 21 | if ( 22 | !resolvedSpritesheet || 23 | !data.spriteMap[resolvedSpritesheet][`${image}.png`] 24 | ) { 25 | resolvedSpritesheet = "quality"; 26 | // eslint-disable-next-line no-param-reassign 27 | image = "quality-unknown"; 28 | } 29 | 30 | const imageData = data.spriteMap[resolvedSpritesheet][`${image}.png`]; 31 | const spritesheetSize = data.spriteMapSizes[resolvedSpritesheet]; 32 | 33 | const url = `${dataPath}${dataPath === "/" ? "" : "/"}${resolvedSpritesheet}.png`; 34 | const scale = !width ? 1 : width / imageData.width; 35 | 36 | return ( 37 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Factoriopedia 2 | 3 | Visit now at [factoriopedia.lukasbach.com](https://factoriopedia.lukasbach.com/)! 4 | 5 | This is a recreation of the Factoriopedia utility in the PC Game Factorio 6 | Space Age, as well as several other tools that are included that may be helpful 7 | to other players. It also includes a full list of all technologies in the game. 8 | 9 | The app is built on a data dump of the game, created by the Factorio binary. 10 | 11 | This repo also contains the tooling to create the data dump, as well as the 12 | UI components used for Factoriopedia as reusable React components that can be 13 | used by the community for other projects. You can find out more about that 14 | [here](https://factoriopedia.lukasbach.com/storybook/?path=/docs/documentation-introduction--docs). 15 | 16 | ## App Screenshots 17 | 18 | ![Screenshot](https://imgur.com/M2FE87V.png) 19 | 20 | ![Screenshot](https://imgur.com/BfZFguK.png) 21 | 22 | ![Screenshot](https://imgur.com/vZ8CrI4.png) 23 | 24 | ![Screenshot](https://imgur.com/hFZPU3u.png) 25 | 26 | ## UI Components Screenshots 27 | 28 | ![Screenshot](https://imgur.com/xam62go.png) 29 | 30 | ![Screenshot](https://imgur.com/nB8Y2WJ.png) 31 | 32 | ## Contributing 33 | 34 | Install volta, then call `yarn` to install all dependencies. 35 | Use `yarn build` to build all packages, and `yarn dev` to start 36 | the local dev server and storybook. 37 | 38 | You will need the Factorio data included in the repo for the app 39 | to run, which is not included in the repo. You can 40 | 41 | - Run a JSON data + locales + assets export with the Factorio binary, 42 | place the data in the `packages/data/script-output` folder and 43 | run `yarn reparse` to generate the data, or 44 | - Run `yarn download-data` to download the latest data from the 45 | NPM data package. 46 | 47 | Before contributing a PR, please run `yarn lint` to ensure your 48 | code matches the linting rules. -------------------------------------------------------------------------------- /packages/ui/src/components/content-section.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | import { Surface } from "./surface"; 3 | import { QualityTiering } from "./quality-tiering"; 4 | 5 | export type ContentSectionVariant = "dark" | "flat" | "inside"; 6 | 7 | export const ContentSectionStat = ({ 8 | label, 9 | children, 10 | skip, 11 | quality, 12 | }: PropsWithChildren<{ 13 | label: string; 14 | skip?: boolean; 15 | quality?: true | [number, number, number, number, number]; 16 | }>) => 17 | skip || !children ? null : ( 18 |

19 | {label}:{" "} 20 | 21 | {quality ? ( 22 | 26 | ) : ( 27 | children 28 | )} 29 | 30 |

31 | ); 32 | 33 | export const ContentSection: FC< 34 | PropsWithChildren<{ title?: string; variant?: ContentSectionVariant }> 35 | > = ({ children, title, variant }) => { 36 | if (variant === "inside") { 37 | return children; 38 | } 39 | 40 | if (variant === "flat") { 41 | return ( 42 |
43 | {title &&
{title}
} 44 |
{children}
45 |
46 | ); 47 | } 48 | 49 | return ( 50 | 55 | {title && ( 56 |
57 | {title} 58 |
59 | )} 60 |
{children}
61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const boxShadow = { 2 | 'inset-1': 'inset 0px 1px 3px 1px rgba(0,0,0,.5)', 3 | 'btnz': 'inset 0px -2px 7px 0px #000000, 0px 2px 4px 1px rgba(0,0,0,0.77), inset 0px 1px 4px -1px rgba(255,255,255,0.8);', 4 | 'orangeglow': 'inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);', 5 | 'deepinset': 'inset 0px 2px 4px 2px rgba(0,0,0,1);', 6 | 'technology': 'inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.7);', 7 | 'topglow-1': 'inset 0px 2px 2px -1px rgba(255,255,255,0.7);', 8 | 'topglow-2': 'inset 0px 1px 3px -1px rgba(255,255,255,0.5);', 9 | 'btn-small': 'inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 1px 2px -1px rgba(255,255,255,0.5);', 10 | 'btn-large': 'inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.5);', 11 | 'grid-backdrop': 'inset 0px 1px 1px -1px rgba(255,255,255,0.5), 0px 1px 1px 1px rgba(0,0,0,0.2);', 12 | }; 13 | 14 | const colors = { 15 | blackDark: "#242324", 16 | blackMedium: "#313031", 17 | blackLight: "#403f40", 18 | grayLight: "#8e8e8e", 19 | orangeDark: "#ffab00", 20 | orangeLight: "#f1be64", 21 | textYellow: "#ddb843", 22 | textGreen: "#299b37", 23 | textBeige: "#e8d2b0", 24 | textBlue: "#77bcda" 25 | }; 26 | const variants = ["hover", "active", "group-hover", "group-active"]; 27 | 28 | /** @type {import('tailwindcss').Config} */ 29 | module.exports = { 30 | content: ["./src/**/*.tsx"], 31 | theme: { 32 | extend: { 33 | colors, 34 | boxShadow 35 | }, 36 | }, 37 | safelist: [ 38 | ...Object.keys(boxShadow).map(value => ({ pattern: new RegExp(`shadow-${value}$`), variants })), 39 | ...Object.keys(colors).map(value => ({ pattern: new RegExp(`bg-${value}$`), variants })), 40 | ], 41 | plugins: [], 42 | } 43 | 44 | -------------------------------------------------------------------------------- /packages/ui/src/components/tabs.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | import * as Tabs from "@radix-ui/react-tabs"; 3 | import * as React from "react"; 4 | import { combine } from "../utils"; 5 | 6 | export const TabsRoot: FC< 7 | Tabs.TabsProps & React.RefAttributes 8 | > = (props) => { 9 | return ; 10 | }; 11 | 12 | export const TabsList: FC< 13 | Tabs.TabsListProps & React.RefAttributes 14 | > = (props) => { 15 | return ; 16 | }; 17 | 18 | export const TabsTriggerVisual: FC< 19 | PropsWithChildren & { active?: boolean }> 20 | > = ({ active, ...props }) => ( 21 | // eslint-disable-next-line jsx-a11y/anchor-has-content 22 | 35 | ); 36 | 37 | export const TabsTrigger: FC< 38 | Tabs.TabsTriggerProps & React.RefAttributes 39 | > = (props) => { 40 | return ( 41 | 50 | ); 51 | }; 52 | 53 | export const TabsContent: FC< 54 | Tabs.TabsContentProps & React.RefAttributes 55 | > = (props) => { 56 | return ; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/ui/src/components/quality-tiering.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import { tooltip } from "./tooltip"; 3 | import { ContentSectionStat } from "./content-section"; 4 | import { TooltipSection } from "./tooltip-section"; 5 | 6 | export const QualityTiering: FC<{ 7 | value: string | string[]; 8 | tiering?: [number, number, number, number, number]; 9 | }> = ({ value, tiering = [1, 1.3, 1.6, 1.9, 2.5] }) => { 10 | const unifiedValue = Array.isArray(value) ? value.join("") : value; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/naming-convention 13 | const [_, baseStr, unit] = useMemo( 14 | () => /^-?([\d.]+)(.*)$/.exec(`${unifiedValue}`) ?? [value, ""], 15 | [unifiedValue, value], 16 | ); 17 | const base = 18 | (unifiedValue?.startsWith?.("-") ? -1 : 1) * Number.parseFloat(baseStr); 19 | return ( 20 | 24 | 25 | 26 | {Math.floor(base * tiering[0])} {unit} 27 | 28 | 29 | {Math.floor(base * tiering[1])} {unit} 30 | 31 | 32 | {Math.floor(base * tiering[2])} {unit} 33 | 34 | 35 | {Math.floor(base * tiering[3])} {unit} 36 | 37 | 38 | {Math.floor(base * tiering[4])} {unit} 39 | 40 | 41 | 42 | Quality values are currently extrapolated from the base value and 43 | might not be accurate. 44 | 45 | , 46 | )} 47 | > 48 | {base} {unit} 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ui/src/stories/data-hooks.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { EntityButton } from "../parts/entity-button"; 3 | import { useFactorioData, useFactorioDataPath } from "../parts/data-provider"; 4 | import { useEntriesOfType } from "../hooks/use-entries-of-type"; 5 | import { useEntry } from "../hooks/use-entry"; 6 | import { useResolveJointItemEntries } from "../hooks/use-resolve-joint-item-entries"; 7 | import { Surface } from "../components/surface"; 8 | 9 | const meta = { 10 | title: "Parts/Data Hooks", 11 | component: EntityButton, 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | 16 | export const LoadingFactorioData = () => ( 17 | 23 | {JSON.stringify( 24 | useFactorioData().entries["fusion-reactor"].recipe.ingredients, 25 | null, 26 | 2, 27 | )} 28 | 29 | ); 30 | 31 | export const LoadingFactorioDataPath = () => ( 32 | 38 | {useFactorioDataPath()} 39 | 40 | ); 41 | 42 | export const UseEntriesOfType = () => ( 43 | 49 | {useEntriesOfType("recipe", (e) => e.merged.name.includes("speed")) 50 | .map((e) => e.merged.name) 51 | .join(" ")} 52 | 53 | ); 54 | 55 | export const UseEntry = () => ( 56 | 62 | {JSON.stringify( 63 | useEntry("speed-module-3", "recipe")?.recipe.ingredients, 64 | null, 65 | 2, 66 | )} 67 | 68 | ); 69 | 70 | export const UseResolvedJointItemEntries = () => ( 71 | 77 | {useResolveJointItemEntries({ group: "logistics", types: ["item"] }) 78 | .reduce((acc, cur) => acc.concat(cur), []) 79 | .map((e) => e.name) 80 | .join(" ")} 81 | 82 | ); 83 | -------------------------------------------------------------------------------- /packages/ui/src/stories/table.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { ColumnDef } from "@tanstack/react-table"; 3 | import { Table } from "../components/table"; 4 | import { Surface } from "../components/surface"; 5 | import { TableCell } from "../components/table-cell"; 6 | 7 | const meta = { 8 | title: "Components/Table", 9 | component: Table, 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | 14 | export const IndividualTableCells = () => ( 15 | 16 |
17 | {}} isTitle> 18 | Cell 1 19 | 20 | {}} isTitle sorting="desc"> 21 | Cell 1 22 | 23 | 24 | Cell 1 25 | 26 |
27 |
28 | Cell 1 29 | Cell 1 30 | Cell 1 31 |
32 |
33 | {}}>Cell 1 34 | {}}>Cell 1 35 | {}}>Cell 1 36 |
37 |
38 | ); 39 | 40 | const columns: ColumnDef<{ a: string; b: string }>[] = [ 41 | { 42 | accessorKey: "a", 43 | cell: (info) => info.getValue(), 44 | header: () => First Cell, 45 | }, 46 | { 47 | accessorKey: "b", 48 | cell: (info) => info.getValue(), 49 | header: () => Second Cell, 50 | }, 51 | ] as const; 52 | 53 | export const TanstackTable = () => ( 54 |
70 | ); 71 | -------------------------------------------------------------------------------- /packages/ui/src/components/button-grid.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, useRef } from "react"; 2 | import { useDebouncedState, useResizeObserver } from "@react-hookz/web"; 3 | import { Surface } from "./surface"; 4 | 5 | export const ButtonGrid: FC< 6 | PropsWithChildren<{ 7 | itemWidth: number; 8 | itemHeight: number; 9 | gridWidth?: number; 10 | gridHeight?: number; 11 | }> 12 | > = ({ children, gridWidth, gridHeight, itemWidth, itemHeight }) => { 13 | const [measures, setMeasures] = useDebouncedState< 14 | { width: number; height: number } | undefined 15 | >(undefined, 200, 1000); 16 | const ref = useRef(null); 17 | useResizeObserver(ref, (entry) => { 18 | setMeasures({ 19 | width: entry.contentRect.width, 20 | height: entry.contentRect.height, 21 | }); 22 | }); 23 | const actualGridWidth = 24 | gridWidth ?? Math.ceil((measures?.width ?? 1) / itemWidth); 25 | const actualGridHeight = 26 | gridHeight ?? Math.ceil((measures?.height ?? 1) / itemHeight); 27 | return ( 28 |
35 | 40 |
44 | {children} 45 |
46 |
47 | {Array.from({ length: actualGridWidth * actualGridHeight }).map( 48 | (_, index) => ( 49 |
40 ? "p-3" : "p-1"} 54 | > 55 |
56 |
57 | ), 58 | )} 59 |
60 | 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/ui/src/parts/entity-button.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, createContext, useContext } from "react"; 2 | import { EntityTooltip } from "./entity-tooltip"; 3 | import { Surface } from "../components/surface"; 4 | import { FactorioImage } from "./factorio-image"; 5 | import { useIsInTooltip } from "../components/tooltip"; 6 | 7 | const EntityButtonActionContext = createContext< 8 | ((name: string, type: string) => void) | undefined 9 | >(undefined); 10 | 11 | export const EntityButtonActionProvider: FC< 12 | PropsWithChildren<{ 13 | onClick: (name: string, type: string) => void; 14 | }> 15 | > = ({ children, onClick }) => ( 16 | 17 | {children} 18 | 19 | ); 20 | 21 | export const EntityButton: FC<{ 22 | name: string; 23 | type: string; 24 | onClick?: () => void; 25 | isActive?: boolean; 26 | dark?: boolean; 27 | subtext?: string; 28 | }> = ({ name, type, onClick, isActive, dark, subtext }) => { 29 | const onClickContext = useContext(EntityButtonActionContext); 30 | const isInTooltip = useIsInTooltip(); 31 | const handler = isInTooltip 32 | ? undefined 33 | : (onClick ?? 34 | (onClickContext ? () => onClickContext(name, type) : undefined)); 35 | const content = ( 36 | 37 | key={name} 38 | as={handler ? "button" : "div"} 39 | onClick={handler} 40 | isActive={isActive} 41 | shadow={dark ? "btn-large" : "btn-small"} 42 | color={dark ? "blackMedium" : "blackLight"} 43 | hover={handler ? { color: "orangeLight" } : {}} 44 | active={handler ? { color: "orangeDark" } : {}} 45 | className="p-0.5 m-0.5 inline-flex items-center justify-center rounded relative" 46 | > 47 | 48 | {subtext && ( 49 |
55 | {subtext} 56 |
57 | )} 58 |
59 | ); 60 | return isInTooltip ? ( 61 | content 62 | ) : ( 63 | 64 | {content} 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /packages/ui/src/stories/0-intro.mdx: -------------------------------------------------------------------------------- 1 | import {Meta, Source, Story} from '@storybook/blocks'; 2 | import * as SurfaceStories from './surface.stories'; 3 | import * as EntityButtonStories from './entity-button.stories'; 4 | 5 | 6 | # Introduction 7 | 8 | This library provides components to build Factorio-related tools. It exposes both a Factorio Data Dump in the 9 | frontend that can be consumed by components and React hooks, as well as visual components that resemble the 10 | Factorio UI. 11 | 12 | This library is not associated or maintained by Wube, but is a fan project. 13 | 14 | # Get Started 15 | 16 | Install the library with npm: 17 | 18 | ```bash 19 | npm install @factorioui/components 20 | ``` 21 | 22 | You can then use the components in your React application: 23 | 24 | ```jsx 25 | import { FactorioDataProvider } from "@factorioui/components"; 26 | 27 | ( 28 | Loading...}> 29 | 30 | 31 | 32 | ); 33 | ``` 34 | 35 | 36 | 37 | When using tooltips, make sure to include the `TooltipRoot` component somewhere in your app. 38 | 39 | # Factorio Data Dump 40 | 41 | The data-related part components of this library require a `FactorioDataProvider` to work. This in turn needs the 42 | JSON data file as well as Factorio assets to be available on a server. This data, exported and generated on Factorio Space Age, 43 | is available as part of the `@factorioui/data` package, which will be included as dependency of this library. 44 | 45 | Make sure to host it as static data with your server. The data will be available in `node_modules/@factorioui/data/data/`. 46 | 47 | When using vite, you can load that data like this in your vite config: 48 | 49 | ```ts 50 | export default defineConfig({ 51 | publicDir: "node_modules/@factorioui/data/data", 52 | plugins: [react()], 53 | }); 54 | ``` 55 | 56 | # Structure of this library 57 | 58 | On the left of this storybook instance, all available components are listed. They are structured as follows: 59 | 60 | - **Components**: Visual components that are just a UI-based implementation and can be used out of the box. 61 | - **Parts**: Components bound to the Factorio Data dump. They need a `FactorioDataProvider` to work. 62 | -------------------------------------------------------------------------------- /packages/ui/src/components/surface.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLProps, PropsWithChildren } from "react"; 2 | import { combine } from "../utils"; 3 | 4 | type SurfaceProperties = { 5 | shadow?: 6 | | "btn-small" 7 | | "orangeglow" 8 | | "deepinset" 9 | | "btn-large" 10 | | "topglow-1" 11 | | "topglow-2" 12 | | "inset-1"; 13 | color?: 14 | | "blackDark" 15 | | "blackMedium" 16 | | "blackLight" 17 | | "grayLight" 18 | | "orangeDark" 19 | | "orangeLight"; 20 | }; 21 | 22 | /* 23 | For tailwind generation: 24 | className="shadow-btn-small shadow-btn-large shadow-orangeglow shadow-deepinset" 25 | className="hover:shadow-btn-small hover:shadow-btn-large hover:shadow-orangeglow hover:shadow-deepinset" 26 | className="active:shadow-btn-small active:shadow-btn-large active:shadow-orangeglow active:shadow-deepinset" 27 | className="bg-blackDark bg-blackMedium bg-blackLight bg-grayLight bg-orangeDark bg-orangeLight" 28 | className="hover:bg-blackDark hover:bg-blackMedium hover:bg-blackLight hover:bg-grayLight hover:bg-orangeDark hover:bg-orangeLight" 29 | className="active:bg-blackDark active:bg-blackMedium active:bg-blackLight active:bg-grayLight active:bg-orangeDark active:bg-orangeLight" 30 | */ 31 | 32 | export type SurfaceProps = PropsWithChildren< 33 | { 34 | hover?: SurfaceProperties; 35 | active?: SurfaceProperties; 36 | isActive?: boolean; 37 | as?: string; 38 | useGroup?: boolean; 39 | } & SurfaceProperties & 40 | HTMLProps 41 | >; 42 | 43 | export function Surface({ 44 | shadow, 45 | color, 46 | hover, 47 | active, 48 | isActive, 49 | as, 50 | useGroup, 51 | ...props 52 | }: SurfaceProps) { 53 | const currentProps = isActive && active ? active : { shadow, color }; 54 | const Comp = (as ?? "div") as any; 55 | const grpref = useGroup ? "group-" : ""; 56 | return ( 57 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /packages/pedia/src/routes/tool.$tool.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | 3 | import { FactorioImage, Table } from "@factorioui/components"; 4 | import { createColumnHelper } from "@tanstack/react-table"; 5 | import { FC } from "react"; 6 | import { TwoColumnContainer } from "../components/two-column-container"; 7 | import { TabbedContentPane } from "../components/tabbed-content-pane"; 8 | import * as tools from "../tools"; 9 | import { useMobileMenu } from "../components/mobile-menu-provider"; 10 | 11 | export const Route = createFileRoute("/tool/$tool")({ 12 | component: Page, 13 | }); 14 | 15 | export type Tool = { 16 | title: string; 17 | id: string; 18 | icon: [string, string]; 19 | render: FC; 20 | }; 21 | 22 | const toolList = Object.entries(tools).map(([id, tool]) => ({ ...tool, id })); 23 | 24 | const columnHelper = createColumnHelper(); 25 | const columns = [ 26 | columnHelper.accessor("icon", { 27 | id: "icon", 28 | header: () => "", 29 | size: 32, 30 | cell: (info) => ( 31 | 36 | ), 37 | }), 38 | columnHelper.accessor("title", { 39 | header: () => "Tool title", 40 | }), 41 | ]; 42 | 43 | function Page() { 44 | const params = Route.useParams(); 45 | const navigate = Route.useNavigate(); 46 | const { closeMenu } = useMobileMenu(); 47 | const tool = (tools as any)[params.tool] as Tool; 48 | const ToolComp = 49 | tool?.render || (() =>
Select a tool on the left.
); 50 | return ( 51 | 54 |
{ 57 | navigate({ 58 | to: "/tool/$tool", 59 | params: { tool: row.original.id }, 60 | }); 61 | closeMenu(); 62 | }} 63 | isRowActive={(row) => row.original.id === params.tool} 64 | data={toolList} 65 | /> 66 | 67 | } 68 | right={ 69 | 72 | 77 | {tool?.title} 78 | 79 | } 80 | tabsList={null} 81 | > 82 | 83 | 84 | } 85 | /> 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@factorioui/components", 3 | "version": "0.0.3", 4 | "description": "UI components based on the Factorio UI, reusable for Factorio Fan Projects", 5 | "author": "Lukas Bach ", 6 | "homepage": "", 7 | "license": "ISC", 8 | "main": "lib/index.js", 9 | "types": "lib/index.d.ts", 10 | "directories": { 11 | "lib": "lib", 12 | "test": "__tests__" 13 | }, 14 | "files": [ 15 | "lib" 16 | ], 17 | "scripts": { 18 | "devx": "concurrently build:watch tailwind:watch storybook", 19 | "prepack": "yarn build", 20 | "build": "tsc && yarn tailwind:build && yarn build-storybook", 21 | "build:watch": "tsc --watch", 22 | "storybook": "storybook dev -p 6006", 23 | "build-storybook": "storybook build", 24 | "tailwind:watch": "tailwindcss -i \"./src/ìnput.css\" -o \"./src/output.css\" --watch", 25 | "tailwind:build": "tailwindcss -i \"./src/ìnput.css\" -o \"./src/output.css\"" 26 | }, 27 | "dependencies": { 28 | "@factorioui/data": "^0.0.3", 29 | "@radix-ui/react-dropdown-menu": "^2.1.2", 30 | "@radix-ui/react-separator": "^1.1.0", 31 | "@radix-ui/react-tabs": "^1.1.1", 32 | "@react-hookz/web": "^24.0.4", 33 | "@tanstack/react-table": "^8.20.6", 34 | "@types/deepmerge": "^2.2.3", 35 | "concurrently": "^9.1.0", 36 | "deepmerge": "^4.3.1", 37 | "react": "18.3.1", 38 | "react-dom": "18.3.1", 39 | "react-tooltip": "^5.28.0", 40 | "tailwindcss": "^3.4.16", 41 | "typescript": "^5.7.2", 42 | "vite": "^6.0.3" 43 | }, 44 | "devDependencies": { 45 | "@chromatic-com/storybook": "3.2.2", 46 | "@storybook/addon-essentials": "8.4.7", 47 | "@storybook/addon-interactions": "8.4.7", 48 | "@storybook/addon-onboarding": "8.4.7", 49 | "@storybook/blocks": "8.4.7", 50 | "@storybook/react": "8.4.7", 51 | "@storybook/react-vite": "8.4.7", 52 | "@storybook/test": "8.4.7", 53 | "@types/prop-types": "^15", 54 | "@types/react": "18.3.1", 55 | "@types/react-dom": "18.3.1", 56 | "prop-types": "15.8.1", 57 | "storybook": "8.4.7" 58 | }, 59 | "funding": "https://github.com/sponsors/lukasbach", 60 | "keywords": [ 61 | "factorio", 62 | "game", 63 | "tool", 64 | "util", 65 | "components", 66 | "ui", 67 | "visual", 68 | "react", 69 | "typescript", 70 | "export", 71 | "spaceage" 72 | ], 73 | "repository": { 74 | "type": "git", 75 | "url": "https://github.com/lukasbach/factoriopedia.git", 76 | "directory": "packages/data" 77 | }, 78 | "publishConfig": { 79 | "access": "public", 80 | "registry": "https://registry.npmjs.org/" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/ui/src/parts/technology-entity-button.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import { 3 | StaticFactorioDataProvider, 4 | useFactorioData, 5 | useFactorioDataPath, 6 | } from "./data-provider"; 7 | import { TechnologyButton } from "../components/technology-button"; 8 | import { FactorioImage } from "./factorio-image"; 9 | import { tooltip } from "../components/tooltip"; 10 | import { useLocaleName } from "./locale-name"; 11 | import { TooltipSection } from "../components/tooltip-section"; 12 | import { EntityButton } from "./entity-button"; 13 | import { useLocaleDescription } from "./locale-description"; 14 | import { ContentSectionStat } from "../components/content-section"; 15 | 16 | export const TechnologyEntityButton: FC<{ 17 | name: string; 18 | onClick?: () => void; 19 | }> = ({ name, onClick }) => { 20 | const { entries } = useFactorioData(); 21 | const tech = entries[name]?.technology; 22 | const description = useLocaleDescription(name); 23 | const unlocks = useMemo( 24 | () => 25 | tech.effects 26 | ?.filter((effect) => effect.type === "unlock-recipe") 27 | .map((effect) => effect.recipe), 28 | [tech.effects], 29 | ); 30 | return ( 31 |
38 | {description && {description}} 39 | {tech.unit?.ingredients?.length > 0 && ( 40 | 41 | {tech.unit?.ingredients.map?.(([ingredient, amount]) => ( 42 | 47 | ))} 48 | x{tech.unit.count ?? tech.unit.count_formula} 49 | 50 | )} 51 | {unlocks?.length > 0 && ( 52 | 53 | 54 | {unlocks.map((unlock) => ( 55 | 56 | ))} 57 | 58 | 59 | )} 60 | , 61 | )} 62 | > 63 | ( 68 | 69 | )) 70 | } 71 | > 72 | 73 | 74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/ui/src/stories/button-grid.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { FactorioImage } from "../parts/factorio-image"; 3 | import { ButtonGrid } from "../components/button-grid"; 4 | import { Surface } from "../components/surface"; 5 | 6 | const meta = { 7 | title: "Components/Button Grid", 8 | component: ButtonGrid, 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | 13 | export const BasicButtonGrid = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export const LargeButtonGrid = () => ( 23 | 24 | 25 | 32 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | export const GridWithoutHeight = () => ( 48 | 49 | 50 | 57 | 58 | 59 | 66 | 67 | 68 | 69 | 70 | ); 71 | -------------------------------------------------------------------------------- /packages/ui/src/components/table.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Cell, 3 | ColumnDef, 4 | Row, 5 | flexRender, 6 | getCoreRowModel, 7 | getSortedRowModel, 8 | useReactTable, 9 | } from "@tanstack/react-table"; 10 | import { TableCell } from "./table-cell"; 11 | 12 | export function Table({ 13 | columns, 14 | data, 15 | onClickCell, 16 | isRowActive, 17 | }: { 18 | columns: ColumnDef[]; 19 | data: T[]; 20 | onClickCell?: (row: Row, cell: Cell) => void; 21 | isRowActive?: (row: Row) => boolean; 22 | }) { 23 | const table = useReactTable({ 24 | columns, 25 | data, 26 | debugTable: true, 27 | getCoreRowModel: getCoreRowModel(), 28 | getSortedRowModel: getSortedRowModel(), 29 | }); 30 | 31 | return ( 32 |
36 | 37 | {table.getHeaderGroups().map((headerGroup) => ( 38 | 39 | {headerGroup.headers.map((header) => { 40 | return ( 41 | 56 | {flexRender( 57 | header.column.columnDef.header, 58 | header.getContext(), 59 | )} 60 | 61 | ); 62 | })} 63 | 64 | ))} 65 | 66 | 67 | {table.getRowModel().rows.map((row) => { 68 | const isActive = isRowActive?.(row); 69 | return ( 70 | 71 | {row.getVisibleCells().map((cell) => { 72 | return ( 73 | onClickCell(row, cell) : undefined 78 | } 79 | style={{ 80 | minWidth: `${cell.column.getSize()}px`, 81 | maxWidth: `${cell.column.getSize()}px`, 82 | width: `${cell.column.getSize()}px`, 83 | }} 84 | > 85 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 86 | 87 | ); 88 | })} 89 | 90 | ); 91 | })} 92 | 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /packages/ui/src/stories/entity-grid.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import React, { useState } from "react"; 3 | import { EntityGrid } from "../parts/entity-grid"; 4 | import { useResolveJointItemEntries } from "../hooks/use-resolve-joint-item-entries"; 5 | import { Surface } from "../components/surface"; 6 | import { GroupTabs } from "../parts/group-tabs"; 7 | import { TooltipRoot } from "../components/tooltip-root"; 8 | import { EntityButton } from "../parts/entity-button"; 9 | 10 | const meta = { 11 | title: "Parts/Entity Grid", 12 | component: EntityGrid, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | 17 | export const WithHardcodedContents = () => { 18 | const [selected, setSelected] = useState(undefined); 19 | return ( 20 | <> 21 | 33 | {JSON.stringify(selected)} 34 | 35 | ); 36 | }; 37 | 38 | export const AllItemsInGroup = () => { 39 | const [selectedGroup, setSelectedGroup] = useState( 40 | "intermediate-products", 41 | ); 42 | return ( 43 |
44 | 45 | 50 | {selectedGroup} 51 | 52 | 60 | 61 |
62 | ); 63 | }; 64 | 65 | export const EntityButtonWithSubtext = () => { 66 | return ( 67 |
68 | 69 | 70 | 71 | 72 | 78 | 84 | 90 | 91 | 92 | 93 | 94 |
95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /packages/ui/src/parts/entity-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | import { tooltip } from "../components/tooltip"; 3 | import { TooltipSection } from "../components/tooltip-section"; 4 | import { 5 | StaticFactorioDataProvider, 6 | useFactorioData, 7 | useFactorioDataPath, 8 | } from "./data-provider"; 9 | import { useLocaleName } from "./locale-name"; 10 | import { useLocaleDescription } from "./locale-description"; 11 | import { EntityButton } from "./entity-button"; 12 | 13 | const TooltipStat = ({ 14 | label, 15 | children, 16 | }: PropsWithChildren<{ label: string }>) => ( 17 |

18 | {label}:{" "} 19 | {children} 20 |

21 | ); 22 | 23 | export const EntityTooltip: FC< 24 | PropsWithChildren<{ name: string; type: string }> 25 | > = ({ name, type, children }) => { 26 | const { entries } = useFactorioData(); 27 | const description = useLocaleDescription(name); 28 | const entry = entries[name][type]; 29 | 30 | return ( 31 | 38 | {description && {description}} 39 | 40 | {type} -{" "} 41 | {name} 42 | 43 | {"recipe" in entries[name] && ( 44 | 45 | {entries[name].recipe.ingredients?.map?.((ingredient) => ( 46 | 51 | ))} 52 | 53 | {entries[name].recipe.results?.map?.((result) => ( 54 | 59 | ))} 60 | 61 | 62 | )} 63 | {"planet" in entries[name] && ( 64 | 65 | 66 | {(entry.surface_properties?.["day-night-cycle"] ?? 1) / 60 / 60}{" "} 67 | minutes 68 | 69 | 70 | {entry.solar_power_in_space} kW 71 | 72 | 73 | {entry.gravity_pull} m/s² 74 | 75 | 76 | {entry.surface_properties?.pressure ?? 1000} hPa 77 | 78 | 79 | )} 80 | , 81 | )} 82 | > 83 | {children} 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /packages/pedia/src/routes/about.lazy.tsx: -------------------------------------------------------------------------------- 1 | import { Link, createLazyFileRoute } from "@tanstack/react-router"; 2 | 3 | import { Table } from "@factorioui/components"; 4 | import { createColumnHelper } from "@tanstack/react-table"; 5 | import { TwoColumnContainer } from "../components/two-column-container"; 6 | import { urls } from "../urls"; 7 | 8 | export const Route = createLazyFileRoute("/about")({ 9 | component: Page, 10 | }); 11 | 12 | type Url = { 13 | title: string; 14 | url: string; 15 | }; 16 | 17 | const data: Url[] = [ 18 | { 19 | title: "Github Repository", 20 | url: urls.repo, 21 | }, 22 | { 23 | title: "Follow me on Github", 24 | url: urls.ghuser, 25 | }, 26 | { 27 | title: "My Homepage", 28 | url: urls.homepage, 29 | }, 30 | { 31 | title: "Factorio UI Components Storybook", 32 | url: urls.storybook, 33 | }, 34 | ]; 35 | 36 | const columnHelper = createColumnHelper(); 37 | const columns = [ 38 | columnHelper.accessor("title", { 39 | header: () => "Relevant Links", 40 | }), 41 | ]; 42 | 43 | function Page() { 44 | return ( 45 | 48 | { 51 | window.open(row.original.url, "_blank"); 52 | }} 53 | data={data} 54 | /> 55 | 56 | } 57 | right={ 58 |
59 |

60 | The primary goal of this project was to experiment with the Factorio 61 | Datadump, and build something nice looking that reflects the 62 | beautiful UI design that Factorio uses. 63 |

64 |

65 | The app uses the JSON data dump provided by the Factorio binary in 66 | Space Age. There are still some bugs and issues in how data is 67 | interpreted from the Factorio data dump, and some pieces of 68 | information are missing since they simply not present in the data 69 | dump. 70 |

71 |

72 | If this tool was helpful to you, please consider{" "} 73 | 78 | starring the repository on Github 79 | 80 | . 81 |

82 |

83 | By the way, I made the UI components used in this project, including 84 | a Frontend Provider that makes the entire Factorio JSON Data and 85 | assets available in the browser, available as React components that 86 | you can use yourself in your own projects. They are deployed on npm 87 | in the package{" "} 88 | 89 | @factorioui/components 90 | {" "} 91 | and can be imported from there. 92 |

93 |

94 | You can find the Storybook for these components{" "} 95 | 100 | here 101 | 102 | . 103 |

104 |
105 | } 106 | /> 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /packages/pedia/src/routes/technology.$name.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import { 3 | ButtonGrid, 4 | EntitySection, 5 | FactorioImage, 6 | LocaleDescription, 7 | LocaleName, 8 | TabsContent, 9 | TabsList, 10 | TabsRoot, 11 | TabsTrigger, 12 | TechnologyEntityButton, 13 | useEntriesOfType, 14 | useFactorioData, 15 | } from "@factorioui/components"; 16 | import { TwoColumnContainer } from "../components/two-column-container"; 17 | import { TabbedContentPane } from "../components/tabbed-content-pane"; 18 | import { useMobileMenu } from "../components/mobile-menu-provider"; 19 | 20 | export const Route = createFileRoute("/technology/$name")({ 21 | component: Page, 22 | }); 23 | 24 | function Page() { 25 | const { entries } = useFactorioData(); 26 | const { name } = Route.useParams(); 27 | const navigate = Route.useNavigate(); 28 | const technology = entries[name]?.technology; 29 | const techs = useEntriesOfType("technology"); 30 | const { closeMenu } = useMobileMenu(); 31 | if (!technology) return null; 32 | return ( 33 | 36 |
37 | {techs.map((tech) => ( 38 | { 41 | navigate({ params: { name: tech.technology.name } }); 42 | closeMenu(); 43 | }} 44 | /> 45 | ))} 46 |
47 | 48 | } 49 | right={ 50 | 51 | 54 | 55 | 56 | 57 | } 58 | tabsList={ 59 | 60 | Technology 61 | Raw Data 62 | 63 | } 64 | > 65 | 66 |
67 | 70 | navigate({ params: { name: technology.name } }) 71 | } 72 | /> 73 |
74 |

75 | 76 |

77 | 82 | 87 |
88 |
89 | 90 | 95 | 100 | 105 |
106 | 107 | 108 | 109 |
110 |
111 | } 112 | /> 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /packages/data/src/scripts/reparse.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-continue,import/no-extraneous-dependencies */ 2 | import fs from "fs-extra"; 3 | import path from "node:path"; 4 | import * as url from "node:url"; 5 | import { glob } from "glob"; 6 | import Spritesmith from "spritesmith"; 7 | import deepmerge from "deepmerge"; 8 | import { DumpType, FactorioType } from "../types/dump"; 9 | import { ignoredTypes } from "./ignored-types"; 10 | import { ignoredProperties } from "./ignored-properties"; 11 | 12 | const targetFolder = path.join( 13 | path.dirname(url.fileURLToPath(import.meta.url)), 14 | "../../data", 15 | ); 16 | 17 | const scriptOutputFolder = path.join( 18 | path.dirname(url.fileURLToPath(import.meta.url)), 19 | "..", 20 | "..", 21 | "script-output", 22 | ); 23 | 24 | if (!(await fs.pathExists(scriptOutputFolder))) { 25 | console.log("Script output folder not found."); 26 | process.exit(1); 27 | } 28 | 29 | await fs.ensureDir(targetFolder); 30 | 31 | const data = await fs.readJson( 32 | path.join(scriptOutputFolder, "data-raw-dump.json"), 33 | ); 34 | 35 | // TODO subgroup map 36 | 37 | const dump: DumpType = { 38 | entries: {}, 39 | locales: { names: {}, descriptions: {} }, 40 | spriteMap: {}, 41 | spriteMapSizes: {}, 42 | typeMap: {}, 43 | subgroupMap: {}, 44 | groupMap: {}, 45 | }; 46 | 47 | for (const [type, typeContent] of Object.entries(data)) { 48 | if (ignoredTypes.includes(type)) continue; 49 | dump.typeMap[type] ??= []; 50 | for (const [name, entity] of Object.entries(typeContent as any)) { 51 | for (const ignorePattern of ignoredProperties) { 52 | for (const property of Object.keys(entity)) { 53 | if (ignorePattern.test(property)) { 54 | delete entity[property]; 55 | } 56 | } 57 | } 58 | 59 | dump.entries[name] ??= {} as any; 60 | // dump.entries[name].merged = deepmerge( 61 | // dump.entries[name].merged || {}, 62 | // entity as any, 63 | // ); 64 | dump.entries[name].types ??= []; 65 | dump.entries[name].types.push(type); 66 | dump.entries[name][type] = entity as FactorioType; 67 | dump.typeMap[type].push(name); 68 | 69 | if ("subgroup" in entity) { 70 | dump.subgroupMap[entity.subgroup] ??= []; 71 | dump.subgroupMap[entity.subgroup].push({ name, type }); 72 | } 73 | if (entity.type === "item-subgroup") { 74 | dump.groupMap[entity.group] ??= []; 75 | dump.groupMap[entity.group].push(name); 76 | } 77 | } 78 | } 79 | 80 | for (const locale of await glob( 81 | path.posix.join(scriptOutputFolder, "*-locale.json"), 82 | )) { 83 | dump.locales = deepmerge(dump.locales, await fs.readJson(locale)); 84 | } 85 | 86 | const sprites = [ 87 | "virtual-signal/*.png", 88 | "recipe/*.png", 89 | "space-connection/*.png", 90 | "space-location/*.png", 91 | "surface/*.png", 92 | "technology/*.png", 93 | "tile/*.png", 94 | "item/*.png", 95 | "item-group/*.png", 96 | "quality/*.png", 97 | "equipment/*.png", 98 | "entity/*.png", 99 | "fluid/*.png", 100 | "achievement/*.png", 101 | "ammo-category/*.png", 102 | "asteroid-chunk/*.png", 103 | ]; 104 | 105 | for (const spriteList of sprites) { 106 | const groupName = path.dirname(spriteList); 107 | const src = await glob(path.posix.join(scriptOutputFolder, spriteList)); 108 | const spriteResult = await new Promise( 109 | (resolve, reject) => { 110 | Spritesmith.run( 111 | { 112 | src, 113 | }, 114 | (err, result) => (err ? reject(err) : resolve(result)), 115 | ); 116 | }, 117 | ); 118 | 119 | await fs.writeFile( 120 | path.join(targetFolder, `${groupName}.png`), 121 | spriteResult.image, 122 | ); 123 | dump.spriteMap[groupName] = Object.fromEntries( 124 | Object.entries(spriteResult.coordinates).map( 125 | ([name, { x, y, width, height }]) => { 126 | return [path.basename(name), { x, y, width, height }]; 127 | }, 128 | {} as Record, 129 | ), 130 | ); 131 | dump.spriteMapSizes[groupName] = { 132 | width: spriteResult.properties.width, 133 | height: spriteResult.properties.height, 134 | }; 135 | } 136 | 137 | await fs.writeJson(path.join(targetFolder, "data.json"), dump, { 138 | spaces: 2, 139 | }); 140 | -------------------------------------------------------------------------------- /packages/pedia/src/routes/pedia.$type.$name.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import { z } from "zod"; 3 | import { 4 | EntityGrid, 5 | EntitySection, 6 | FactorioImage, 7 | GroupTabs, 8 | LocaleName, 9 | Surface, 10 | useResolveJointItemEntries, 11 | } from "@factorioui/components"; 12 | import { 13 | TabsContent, 14 | TabsList, 15 | TabsRoot, 16 | TabsTrigger, 17 | } from "@factorioui/components/lib/components/tabs"; 18 | import { TwoColumnContainer } from "../components/two-column-container"; 19 | import { TabbedContentPane } from "../components/tabbed-content-pane"; 20 | import { useMobileMenu } from "../components/mobile-menu-provider"; 21 | 22 | const PediaSearchSchema = z.object({ 23 | group: z.string().catch("logistics"), 24 | }); 25 | 26 | export const Route = createFileRoute("/pedia/$type/$name")({ 27 | component: Page, 28 | validateSearch: PediaSearchSchema, 29 | }); 30 | 31 | function Page() { 32 | const { name, type } = Route.useParams(); 33 | const { group } = Route.useSearch(); 34 | const navigate = Route.useNavigate(); 35 | const { closeMenu } = useMobileMenu(); 36 | return ( 37 | 40 |
41 | 45 | navigate({ params: { name, type }, search: { group } }) 46 | } 47 | /> 48 |
49 | 50 | 55 | { 60 | navigate({ params: { name, type }, search: { group } }); 61 | closeMenu(); 62 | }} 63 | items={useResolveJointItemEntries({ 64 | group, 65 | }).map((subgroup) => 66 | subgroup.map(({ name, type }) => ({ 67 | name, 68 | type, 69 | })), 70 | )} 71 | /> 72 | 73 | 74 | } 75 | right={ 76 | 77 | 80 | 81 | 82 | 83 | } 84 | tabsList={ 85 | 86 | Entity 87 | Raw Data 88 | 89 | } 90 | > 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | } 119 | /> 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /packages/ui/src/stories/surface.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@storybook/react"; 2 | import { useState } from "react"; 3 | import { Surface } from "../components/surface"; 4 | import { FactorioImage } from "../parts/factorio-image"; 5 | import { useResolveJointItemEntries } from "../hooks/use-resolve-joint-item-entries"; 6 | import { useEntriesOfType } from "../hooks/use-entries-of-type"; 7 | 8 | const meta = { 9 | title: "Components/Surface", 10 | component: Surface, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | const shadows = [ 16 | "btn-small", 17 | "orangeglow", 18 | "deepinset", 19 | "btn-large", 20 | "topglow-1", 21 | "topglow-2", 22 | "inset-1", 23 | ]; 24 | const colors = [ 25 | "blackDark", 26 | "blackMedium", 27 | "blackLight", 28 | "grayLight", 29 | "orangeDark", 30 | "orangeLight", 31 | ]; 32 | 33 | export const AllSurfaces = () => ( 34 | <> 35 | {colors.map((color) => ( 36 |
37 | {shadows.map((shadow) => ( 38 | 44 | color = {color} 45 |
46 | shadow = {shadow} 47 |
48 | ))} 49 |
50 | ))} 51 | 52 | ); 53 | 54 | export const ItemPlates = () => ( 55 |
56 | 63 | 64 | 65 | 70 | 71 | 72 |
73 | ); 74 | 75 | export const GroupButtons = () => ( 76 |
77 | {useEntriesOfType("item-group").map((group) => ( 78 | 86 | 87 | 88 | ))} 89 |
90 | ); 91 | 92 | export const ItemsInGroup = () => ( 93 |
94 | {useResolveJointItemEntries({ 95 | group: "intermediate-products", 96 | types: ["item", "tool", "recipe"], 97 | }).map((subgroup) => ( 98 |
99 | {subgroup.map(({ entry }) => ( 100 | 109 | 110 | 111 | ))} 112 |
113 | ))} 114 |
115 | ); 116 | 117 | export const ClickableBoxes = () => { 118 | const [isActive, setIsActive] = useState(false); 119 | 120 | return ( 121 | setIsActive(!isActive)} 133 | > 134 |
135 | 136 |
137 |
138 |
123
139 |
600%
140 |
141 |
142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /packages/pedia/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Link, 3 | Outlet, 4 | createRootRoute, 5 | useMatchRoute, 6 | } from "@tanstack/react-router"; 7 | // eslint-disable-next-line import/no-extraneous-dependencies 8 | import { 9 | EntityButtonActionProvider, 10 | FactorioDataProvider, 11 | Surface, 12 | TabsTriggerVisual, 13 | TooltipRoot, 14 | } from "@factorioui/components"; 15 | import { useMemo, useState } from "react"; 16 | import { Route as PediaRoute } from "./pedia.$type.$name"; 17 | import { urls } from "../urls"; 18 | import { MobileMenuProvider } from "../components/mobile-menu-provider"; 19 | 20 | export const Route = createRootRoute({ 21 | component: Page, 22 | }); 23 | 24 | function Page() { 25 | const navigate = PediaRoute.useNavigate(); 26 | const search = Route.useSearch(); 27 | const matchRoute = useMatchRoute(); 28 | const [isMenuOpen, setIsMenuOpen] = useState(false); 29 | 30 | const openMenu = () => setIsMenuOpen(true); 31 | 32 | return ( 33 | ({ 36 | isMenuOpen, 37 | openMenu: () => setIsMenuOpen(true), 38 | closeMenu: () => setIsMenuOpen(false), 39 | }), 40 | [isMenuOpen], 41 | )} 42 | > 43 | Loading...}> 44 | { 46 | if (type === "technology") { 47 | navigate({ params: { name }, to: "/technology/$name" }); 48 | setIsMenuOpen(false); 49 | } else { 50 | navigate({ 51 | params: { name, type }, 52 | search, 53 | to: "/pedia/$type/$name", 54 | }); 55 | setIsMenuOpen(false); 56 | } 57 | }} 58 | > 59 | 63 |
64 |
65 | setIsMenuOpen(!isMenuOpen)} 68 | > 69 | Menu 70 | 71 |
72 | 77 | 80 | Factoriopedia 81 | 82 | 83 | 88 | 91 | Technologies 92 | 93 | 94 | 99 | 100 | Tools 101 | 102 | 103 | 104 | 105 | About 106 | 107 | 108 |
109 | 114 | Star on Github 115 | 116 |
117 | 118 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /packages/data/src/scripts/ignored-types.ts: -------------------------------------------------------------------------------- 1 | export const ignoredTypes = [ 2 | "font", 3 | "gui-style", 4 | "utility-constants", 5 | "utility-sounds", 6 | "sprite", 7 | "utility-sprites", 8 | // "god-controller", 9 | // "editor-controller", 10 | // "spectator-controller", 11 | // "remote-controller", 12 | // "noise-function", 13 | // "noise-expression", 14 | // "mouse-cursor", 15 | // "virtual-signal", 16 | "entity-ghost", 17 | // "item", 18 | // "recipe", 19 | // "quality", 20 | // "fluid", 21 | // "tile", 22 | // "space-location", 23 | // "asteroid-chunk", 24 | // "recipe-category", 25 | // "burner-usage", 26 | "damage-type", 27 | "ambient-sound", 28 | "collision-layer", 29 | // "container", 30 | "deliver-impact-combination", 31 | // "simple-entity", 32 | "explosion", 33 | "character-corpse", 34 | "optimized-particle", 35 | // "character", 36 | // "furnace", 37 | // "fish", 38 | // "boiler", 39 | // "electric-pole", 40 | // "generator", 41 | // "offshore-pump", 42 | // "inserter", 43 | // "item-entity", 44 | // "pipe", 45 | // "radar", 46 | // "lamp", 47 | // "arrow", 48 | // "pipe-to-ground", 49 | // "assembling-machine", 50 | // "tile-ghost", 51 | // "deconstructible-tile-proxy", 52 | // "item-request-proxy", 53 | // "cliff", 54 | // "wall", 55 | // "lab", 56 | // "highlight-box", 57 | // "car", 58 | // "gate", 59 | // "solar-panel", 60 | // "accumulator", 61 | // "electric-energy-interface", 62 | // "land-mine", 63 | // "logistic-container", 64 | // "rocket-silo", 65 | // "rocket-silo-rocket", 66 | "rocket-silo-rocket-shadow", 67 | // "cargo-landing-pad", 68 | // "roboport", 69 | // "storage-tank", 70 | // "pump", 71 | // "market", 72 | // "beacon", 73 | "smoke-with-trigger", 74 | "sticker", 75 | // "reactor", 76 | // "heat-pipe", 77 | // "simple-entity-with-force", 78 | // "simple-entity-with-owner", 79 | "artillery-flare", 80 | "speech-bubble", 81 | // "spider-vehicle", 82 | "spider-leg", 83 | // "infinity-container", 84 | // "infinity-pipe", 85 | // "burner-generator", 86 | // "heat-interface", 87 | // "linked-container", 88 | // "cargo-pod", 89 | // "temporary-container", 90 | // "transport-belt", 91 | // "underground-belt", 92 | // "splitter", 93 | // "loader", 94 | // "loader-1x1", 95 | // "linked-belt", 96 | // "lane-splitter", 97 | "delayed-active-trigger", 98 | // "custom-input", 99 | "fire", 100 | // "mining-drill", 101 | "particle-source", 102 | "stream", 103 | // "resource", 104 | // "turret", 105 | // "ammo-turret", 106 | "corpse", 107 | // "electric-turret", 108 | // "artillery-turret", 109 | // "unit", 110 | // "unit-spawner", 111 | // "spider-unit", 112 | // "legacy-straight-rail", 113 | // "legacy-curved-rail", 114 | // "straight-rail", 115 | // "half-diagonal-rail", 116 | // "curved-rail-a", 117 | // "curved-rail-b", 118 | // "rail-ramp", 119 | // "elevated-straight-rail", 120 | // "elevated-half-diagonal-rail", 121 | // "elevated-curved-rail-a", 122 | // "elevated-curved-rail-b", 123 | // "rail-support", 124 | // "locomotive", 125 | // "cargo-wagon", 126 | // "fluid-wagon", 127 | // "artillery-wagon", 128 | // "train-stop", 129 | // "rail-signal", 130 | // "rail-chain-signal", 131 | "rail-remnants", 132 | // "tree", 133 | "trivial-smoke", 134 | // "combat-robot", 135 | // "construction-robot", 136 | // "logistic-robot", 137 | // "capsule", 138 | // "repair-tool", 139 | // "copy-paste-tool", 140 | // "blueprint", 141 | // "tool", 142 | // "item-with-entity-data", 143 | // "rail-planner", 144 | // "upgrade-item", 145 | // "deconstruction-item", 146 | // "blueprint-book", 147 | // "spidertron-remote", 148 | // "selection-tool", 149 | // "module", 150 | // "ammo", 151 | // "gun", 152 | // "armor", 153 | // "item-group", 154 | // "item-subgroup", 155 | // "autoplace-control", 156 | // "map-settings", 157 | // "map-gen-presets", 158 | "tile-effect", 159 | "optimized-decorative", 160 | // "ammo-category", 161 | // "fuel-category", 162 | // "resource-category", 163 | // "module-category", 164 | // "equipment-grid", 165 | // "equipment-category", 166 | // "shortcut", 167 | "trigger-target-type", 168 | // "projectile", 169 | // "artillery-projectile", 170 | // "beam", 171 | // "technology", 172 | "tips-and-tricks-item-category", 173 | "tips-and-tricks-item", 174 | // "build-entity-achievement", 175 | // "research-achievement", 176 | // "use-entity-in-energy-production-achievement", 177 | // "produce-achievement", 178 | // "complete-objective-achievement", 179 | // "group-attack-achievement", 180 | // "construct-with-robots-achievement", 181 | // "deconstruct-with-robots-achievement", 182 | // "deliver-by-robots-achievement", 183 | // "train-path-achievement", 184 | // "player-damaged-achievement", 185 | // "deplete-resource-achievement", 186 | // "produce-per-hour-achievement", 187 | // "dont-use-entity-in-energy-production-achievement", 188 | // "research-with-science-pack-achievement", 189 | // "kill-achievement", 190 | // "destroy-cliff-achievement", 191 | // "shoot-achievement", 192 | // "combat-robot-count-achievement", 193 | // "dont-kill-manually-achievement", 194 | // "dont-craft-manually-achievement", 195 | // "dont-build-entity-achievement", 196 | // "achievement", 197 | // "airborne-pollutant", 198 | // "fluid-turret", 199 | // "arithmetic-combinator", 200 | // "decider-combinator", 201 | // "constant-combinator", 202 | // "programmable-speaker", 203 | // "power-switch", 204 | // "display-panel", 205 | // "selector-combinator", 206 | "tutorial", 207 | // "night-vision-equipment", 208 | // "energy-shield-equipment", 209 | // "battery-equipment", 210 | // "solar-panel-equipment", 211 | // "generator-equipment", 212 | // "active-defense-equipment", 213 | // "movement-bonus-equipment", 214 | // "roboport-equipment", 215 | // "belt-immunity-equipment", 216 | // "equipment-ghost", 217 | // "planet", 218 | // "surface-property", 219 | "procession-layer-inheritance-group", 220 | "procession", 221 | "impact-category", 222 | "deliver-category", 223 | "chain-active-trigger", 224 | // "space-platform-hub", 225 | // "cargo-bay", 226 | // "asteroid-collector", 227 | // "thruster", 228 | // "agricultural-tower", 229 | // "lightning-attractor", 230 | // "fusion-generator", 231 | // "fusion-reactor", 232 | // "lightning", 233 | // "segmented-unit", 234 | // "segment", 235 | // "plant", 236 | // "capture-robot", 237 | // "asteroid", 238 | // "space-platform-starter-pack", 239 | // "create-platform-achievement", 240 | // "change-surface-achievement", 241 | // "space-connection-distance-traveled-achievement", 242 | // "dont-research-before-researching-achievement", 243 | // "module-transfer-achievement", 244 | // "equip-armor-achievement", 245 | // "use-item-achievement", 246 | // "place-equipment-achievement", 247 | // "inventory-bonus-equipment", 248 | // "space-connection", 249 | // "surface", 250 | ]; 251 | -------------------------------------------------------------------------------- /packages/pedia/src/tools/fusionratio.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useMemo, useState } from "react"; 2 | import { 3 | FactorioImage, 4 | Ingredient, 5 | Surface, 6 | TooltipSection, 7 | tooltip, 8 | } from "@factorioui/components"; 9 | import { Tool } from "../routes/tool.$tool"; 10 | 11 | const itemWidth = 64; 12 | const itemHeight = 48; 13 | 14 | const floor = (n: number) => Math.floor(n * 100) / 100; 15 | 16 | const getAt = (checks: boolean[], width: number, x: number, y: number) => { 17 | if (x < 0 || x >= width || y < 0 || y >= Math.floor(checks.length / width)) { 18 | return false; 19 | } 20 | return checks[y * width + x]; 21 | }; 22 | 23 | const getNeighborCount = (checks: boolean[], width: number, i: number) => { 24 | const x = i % width; 25 | const y = Math.floor(i / width); 26 | let count = 0; 27 | if (x % 2 === 0) { 28 | if (getAt(checks, width, x - 1, y)) count++; 29 | if (getAt(checks, width, x - 1, y + 1)) count++; 30 | if (getAt(checks, width, x, y - 1)) count++; 31 | if (getAt(checks, width, x, y + 1)) count++; 32 | if (getAt(checks, width, x + 1, y)) count++; 33 | if (getAt(checks, width, x + 1, y + 1)) count++; 34 | } else { 35 | if (getAt(checks, width, x - 1, y - 1)) count++; 36 | if (getAt(checks, width, x - 1, y)) count++; 37 | if (getAt(checks, width, x, y - 1)) count++; 38 | if (getAt(checks, width, x, y + 1)) count++; 39 | if (getAt(checks, width, x + 1, y - 1)) count++; 40 | if (getAt(checks, width, x + 1, y)) count++; 41 | } 42 | return count; 43 | }; 44 | 45 | const ToolRender: FC = () => { 46 | const [width] = useState(10); 47 | const [height] = useState(6); 48 | const [checks, setChecks] = useState(() => Array(width * height).fill(false)); 49 | const neighborCounts = useMemo( 50 | () => checks.map((_, i) => getNeighborCount(checks, width, i)), 51 | [checks, width], 52 | ); 53 | const totalReactors = useMemo(() => checks.filter((c) => c).length, [checks]); 54 | const totalGenerators = useMemo( 55 | () => 56 | checks 57 | .map((c, i) => (c ? getNeighborCount(checks, width, i) + 1 : 0)) 58 | .reduce((acc, val) => acc + val, 0) * 2, 59 | [checks, width], 60 | ); 61 | const avgEfficiency = useMemo( 62 | () => 63 | totalReactors === 0 64 | ? 0 65 | : checks 66 | .map((check, i) => 67 | check ? 1 + getNeighborCount(checks, width, i) : 0, 68 | ) 69 | .reduce((acc, val) => acc + val, 0) / totalReactors, 70 | [checks, totalReactors, width], 71 | ); 72 | 73 | useEffect(() => { 74 | const newChecks = Array(width * height).fill(false); 75 | for (let i = 0; i < Math.min(checks.length, newChecks.length); i++) { 76 | newChecks[i] = checks[i]; 77 | } 78 | setChecks(newChecks); 79 | // eslint-disable-next-line react-hooks/exhaustive-deps 80 | }, [width, height]); 81 | 82 | return ( 83 | <> 84 |
88 | {checks.map((checked, i) => ( 89 | { 109 | const newChecks = [...checks]; 110 | newChecks[i] = !newChecks[i]; 111 | setChecks(newChecks); 112 | }} 113 | {...tooltip( 114 | checked 115 | ? `Reactor at ${i % width}.${Math.floor(i / width)}` 116 | : "Click to add reactor here", 117 | checked ? ( 118 | <> 119 | 120 | Will run at{" "} 121 | 122 | {(1 + neighborCounts[i]) * 100}% 123 | {" "} 124 | efficiency, requiring{" "} 125 | 126 | {(1 + neighborCounts[i]) * 2} generator 127 | 128 | 129 | 130 | 131 | {neighborCounts[i]} neighbor 132 | {neighborCounts[i] === 1 ? "" : "s"} 133 | 134 | 135 | 136 | ) : ( 137 |
138 | ), 139 | )} 140 | > 141 |
142 | 143 |
144 | {checked && ( 145 |
146 |
{floor((1 + neighborCounts[i]) * 2)}
147 |
{floor((1 + neighborCounts[i]) * 100)}%
148 |
149 | )} 150 | {/* false && ( 151 |
152 |
153 | {i % width}.{Math.floor(i / width)} 154 |
155 |
N{neighborCounts[i]}
156 |
157 | ) */} 158 | 159 | ))} 160 |
161 |

162 | The top count in each cell determines how many reactors that generator 163 | needs. Your setup will generate{" "} 164 | 165 | {floor(totalGenerators * 0.05)}GW 166 | 167 | , or{" "} 168 | 169 | {floor(totalGenerators * 0.125)}GW 170 | {" "} 171 | for legendary buildings. Your reactors have a average efficiency of{" "} 172 | 173 | {floor(avgEfficiency * 100)}% 174 | 175 |

176 |

You will need:

177 | 178 | 179 | 185 | 191 | 192 | ); 193 | }; 194 | 195 | export const fusionratio: Omit = { 196 | title: "Fusion Ratio Calculator", 197 | icon: ["item", "fusion-generator"], 198 | render: ToolRender, 199 | }; 200 | -------------------------------------------------------------------------------- /packages/ui/src/parts/entity-sections.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from "react"; 2 | import { DumpType } from "@factorioui/data"; 3 | import { 4 | ContentSection, 5 | ContentSectionStat, 6 | ContentSectionVariant, 7 | } from "../components/content-section"; 8 | import { LocaleDescription } from "./locale-description"; 9 | import { Ingredient } from "./ingredient"; 10 | import { useEntry } from "../hooks/use-entry"; 11 | import { useFactorioData } from "./data-provider"; 12 | import { EntityGrid } from "./entity-grid"; 13 | import { EntityButton } from "./entity-button"; 14 | 15 | type Props = { 16 | variant?: ContentSectionVariant; 17 | name: string; 18 | type: string; 19 | }; 20 | 21 | const showAlways = () => true; 22 | 23 | const makeSection = ( 24 | title: string | undefined, 25 | compute: ( 26 | entry: DumpType["entries"][string], 27 | data: DumpType, 28 | ) => T | false | null, 29 | Component: FC< 30 | Props & { 31 | entry: DumpType["entries"][string]; 32 | result: T; 33 | data: DumpType; 34 | } 35 | >, 36 | ) => 37 | memo((props: Props) => { 38 | const data = useFactorioData(); 39 | const entry = useEntry(props.name, props.type); 40 | if (!entry) return null; 41 | const result = compute(entry, data); 42 | return !result || (Array.isArray(result) && !result.length) ? null : ( 43 | 44 | 45 | 46 | ); 47 | }); 48 | 49 | const ItemInfo = makeSection(undefined, showAlways, ({ entry }) => ( 50 | <> 51 | 52 | {entry.types.join(", ")} 53 | 54 | 55 | {entry.merged.max_health} 56 | 57 | 58 | {entry.merged.stack_size} 59 | 60 | 61 | {entry.merged.weight 62 | ? 1000000 / entry.merged.weight 63 | : entry.merged.stack_size} 64 | 65 | 66 | {entry.merged.inventory_size} 67 | 68 | 72 | {entry.merged.filter_count} filters 73 | 74 | 78 | {entry.merged.filter_count} filters 79 | 80 | 84 | {entry.merged.heating_energy} 85 | 86 | 87 | )); 88 | 89 | const CanBurnIn = makeSection( 90 | "Can burn in", 91 | (entry, data) => 92 | entry.item.fuel_category 93 | ? Object.values(data.entries).filter((e) => 94 | e.merged.energy_source?.fuel_categories?.includes( 95 | entry.item.fuel_category, 96 | ), 97 | ) 98 | : [], 99 | ({ result }) => ( 100 | ({ name: e.merged.name, type: "item" }))]} 102 | /> 103 | ), 104 | ); 105 | 106 | const FuelDetails = makeSection( 107 | "Fuel", 108 | (entry) => !!entry.item?.fuel_category, 109 | ({ entry, name, type }) => ( 110 | <> 111 | 112 | {entry.item.fuel_value} GJ 113 | 114 | 115 | {Math.round(entry.item.fuel_acceleration_multiplier * 100)}% 116 | 117 | 118 | {Math.round(entry.item.fuel_top_speed_multiplier * 100)}% 119 | 120 | 121 | 122 | 123 | ), 124 | ); 125 | 126 | const AlternativeRecipes = makeSection( 127 | "Alternative Recipes", 128 | (entry, data) => 129 | Object.values(data.entries).filter( 130 | (e) => 131 | e.merged.type === "recipe" && 132 | e.recipe.name !== entry.merged.name && 133 | e.recipe.results.some?.((r) => r.name === entry.merged.name), 134 | ), 135 | ({ result }) => ( 136 | ({ name: e.merged.name, type: "recipe" }))]} 138 | /> 139 | ), 140 | ); 141 | 142 | const ModuleDetails = makeSection( 143 | "Module Effects", 144 | (entry) => entry.module?.effect, 145 | ({ result }) => ( 146 | <> 147 | 148 | {result.speed * 100}% 149 | 150 | 155 | {result.consumption * 100}% 156 | 157 | 158 | {result.pollution * 100}% 159 | 160 | 165 | {result.productivity * 100}% 166 | 167 | 168 | {result.quality * 100}% 169 | 170 | 171 | ), 172 | ); 173 | 174 | const MadeIn = makeSection( 175 | "Made In", 176 | (entry, data) => 177 | entry.recipe?.category && 178 | Object.values(data.entries).filter((e) => 179 | e["assembling-machine"]?.crafting_categories?.includes( 180 | entry.recipe.category, 181 | ), 182 | ), 183 | ({ result }) => ( 184 | ({ name: e.merged.name, type: "recipe" }))]} 186 | /> 187 | ), 188 | ); 189 | 190 | const UsedIn = makeSection( 191 | "Used In", 192 | (entry, data) => 193 | Object.values(data.entries).filter((e) => 194 | e.recipe?.ingredients?.some?.( 195 | (ingredient) => ingredient.name === entry.merged.name, 196 | ), 197 | ), 198 | ({ result }) => ( 199 | ({ name: e.merged.name, type: "recipe" }))]} 201 | /> 202 | ), 203 | ); 204 | 205 | const Recipe = makeSection( 206 | "Ingredients", 207 | (entry) => entry.recipe, 208 | ({ entry }) => ( 209 | <> 210 | {entry.recipe.ingredients?.map((ingredient) => ( 211 | 216 | ))} 217 |
218 |
219 | 220 | {entry.merged.energy_required || "0.5"}s 221 | {" "} 222 | Crafting time 223 |
224 | 1 ? "Results" : "Result"} 226 | > 227 | {entry.recipe.results?.map((result) => ( 228 | 235 | ))} 236 | 237 | 238 | ), 239 | ); 240 | 241 | const AppearsOn = makeSection( 242 | "Appears on", 243 | (entry, data) => 244 | Object.values(data.entries ?? {}).filter((e) => 245 | Object.keys( 246 | e.planet?.map_gen_settings?.autoplace_controls ?? {}, 247 | ).includes(entry.merged.name), 248 | ), 249 | ({ result }) => { 250 | // TODO doesnt work great 251 | return ( 252 | ({ name: e.merged.name, type: "planet" }))]} 254 | /> 255 | ); 256 | }, 257 | ); 258 | 259 | const EquipmentGridPlaceable = makeSection( 260 | "Placed in equipment grid", 261 | (entry) => entry.merged.shape, 262 | ({ result }) => { 263 | return ( 264 | 265 | {result.width}x{result.height} 266 | 267 | ); 268 | }, 269 | ); 270 | 271 | const Electricity = makeSection( 272 | "Electricity", 273 | (entry) => 274 | entry.merged.energy_source?.type === "electric" 275 | ? entry.merged.energy_source 276 | : false, 277 | ({ result, entry }) => { 278 | return ( 279 | <> 280 | 281 | {result.buffer_capacity} 282 | 283 | 284 | {entry.merged.production} 285 | 286 | 287 | {entry.merged.power_input || entry.merged.energy_usage} 288 | 289 | 290 | {result.output_flow_limit} 291 | 292 | 293 | {result.drain} 294 | 295 | 296 | {entry.merged.energy_per_movement} 297 | 298 | 299 | {entry.merged.energy_per_rotation} 300 | 301 | 302 | ); 303 | }, 304 | ); 305 | 306 | const TechCost = makeSection( 307 | "Cost", 308 | (entry) => entry.technology.unit?.ingredients, 309 | ({ entry, result }) => { 310 | return ( 311 | <> 312 | ({ name, type: "recipe" }))]} 314 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 315 | subtexts={[result.map(([_, amount]) => amount)]} 316 | /> 317 | x{entry.technology.unit.count ?? entry.technology.unit.count_formula} 318 | 319 | ); 320 | }, 321 | ); 322 | 323 | const TechUnlocksRecipes = makeSection( 324 | "Unlocks recipes", 325 | (entry) => 326 | entry.technology.effects 327 | ?.filter((effect) => effect.type === "unlock-recipe") 328 | .map((effect) => effect.recipe), 329 | ({ result }) => { 330 | return ( 331 | ({ name, type: "recipe" }))]} /> 332 | ); 333 | }, 334 | ); 335 | 336 | const TechPrerequisites = makeSection( 337 | "Prerequisites", 338 | (entry) => entry.technology.prerequisites, 339 | ({ result }) => { 340 | return ( 341 | ({ name, type: "technology" }))]} 343 | /> 344 | ); 345 | }, 346 | ); 347 | 348 | const TechPrerequisiteFor = makeSection( 349 | "Prerequisite for", 350 | (entry, data) => 351 | data.typeMap.technology.filter((name) => 352 | data.entries[name].technology.prerequisites?.includes(entry.merged.name), 353 | ), 354 | ({ result }) => { 355 | return ( 356 | ({ name, type: "technology" }))]} 358 | /> 359 | ); 360 | }, 361 | ); 362 | 363 | const TechResearchTrigger = makeSection( 364 | "Researched by", 365 | (entry) => entry.technology.research_trigger, 366 | ({ result }) => { 367 | return ( 368 | <> 369 | 373 | 378 | 379 | 383 | 388 | 389 | 393 | Creating space platform 394 | 395 | 399 | Capturing spawner 400 | 401 | 402 | ); 403 | }, 404 | ); 405 | 406 | // TODO 407 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 408 | const TechResearchTriggerMine = makeSection( 409 | "Triggered by mining", 410 | (entry) => 411 | entry.technology.research_trigger 412 | ?.filter((t) => t.type === "mine-entity") 413 | .map((t) => t.entity), 414 | ({ result }) => { 415 | return ( 416 | ({ name, type: "resource" }))]} 418 | /> 419 | ); 420 | }, 421 | ); 422 | 423 | // TODO 424 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 425 | const TechResearchTriggerCraft = makeSection( 426 | "Triggered by crafting", 427 | (entry) => 428 | entry.technology.research_trigger 429 | ?.filter((t) => t.type === "craft-item") 430 | .map((t) => t.entity), 431 | ({ result }) => { 432 | return ( 433 | ({ name: t.entity, type: "recipe" }))]} 435 | subtexts={[result.map((t) => t.count)]} 436 | /> 437 | ); 438 | }, 439 | ); 440 | 441 | const CanCraft = makeSection( 442 | "Can Craft", 443 | (entry) => 444 | entry.types.includes("assembling-machine") || 445 | entry.types.includes("furnace"), 446 | ({ entry }) => { 447 | return ( 448 | <> 449 | 450 | {entry.merged.crafting_speed || 1}x 451 | 452 | 453 | {entry.merged.module_slots} 454 | 455 | 456 | {entry.merged.allowed_effects.includes("productivity") && ( 457 | 458 | )} 459 | {entry.merged.allowed_effects.includes("speed") && ( 460 | 461 | )} 462 | {entry.merged.allowed_effects.includes("consumption") && ( 463 | 464 | )} 465 | {entry.merged.allowed_effects.includes("quality") && ( 466 | 467 | )} 468 | 469 | 470 | ); 471 | }, 472 | ); 473 | 474 | const CanCraftItemList = makeSection( 475 | "Can Craft Items", 476 | (entry) => 477 | entry.types.includes("assembling-machine") || 478 | entry.types.includes("furnace") 479 | ? entry.merged.crafting_categories 480 | : undefined, 481 | ({ result, data }) => { 482 | return ( 483 | 485 | Object.values(data.entries) 486 | .filter((e) => e.merged.category === category) 487 | .map((e) => ({ name: e.merged.name, type: "recipe" })), 488 | )} 489 | /> 490 | ); 491 | }, 492 | ); 493 | 494 | const Debug = makeSection( 495 | "Debug Data", 496 | () => true, 497 | ({ entry }) => { 498 | return ( 499 |
503 |         {JSON.stringify(entry, null, 2)}
504 |       
505 | ); 506 | }, 507 | ); 508 | 509 | const Main = makeSection( 510 | undefined, 511 | () => true, 512 | ({ entry }) => { 513 | return ( 514 | <> 515 | 516 |
517 | 522 | 523 | ); 524 | }, 525 | ); 526 | 527 | export const EntitySection = { 528 | Main, 529 | Debug, 530 | Recipe, 531 | AlternativeRecipes, 532 | MadeIn, 533 | UsedIn, 534 | AppearsOn, 535 | EquipmentGridPlaceable, 536 | Electricity, 537 | TechCost, 538 | TechUnlocksRecipes, 539 | TechPrerequisites, 540 | TechPrerequisiteFor, 541 | TechResearchTrigger, 542 | CanCraft, 543 | CanCraftItemList, 544 | FuelDetails, 545 | ModuleDetails, 546 | }; 547 | -------------------------------------------------------------------------------- /packages/ui/src/output.css: -------------------------------------------------------------------------------- 1 | *, ::before, ::after { 2 | --tw-border-spacing-x: 0; 3 | --tw-border-spacing-y: 0; 4 | --tw-translate-x: 0; 5 | --tw-translate-y: 0; 6 | --tw-rotate: 0; 7 | --tw-skew-x: 0; 8 | --tw-skew-y: 0; 9 | --tw-scale-x: 1; 10 | --tw-scale-y: 1; 11 | --tw-pan-x: ; 12 | --tw-pan-y: ; 13 | --tw-pinch-zoom: ; 14 | --tw-scroll-snap-strictness: proximity; 15 | --tw-gradient-from-position: ; 16 | --tw-gradient-via-position: ; 17 | --tw-gradient-to-position: ; 18 | --tw-ordinal: ; 19 | --tw-slashed-zero: ; 20 | --tw-numeric-figure: ; 21 | --tw-numeric-spacing: ; 22 | --tw-numeric-fraction: ; 23 | --tw-ring-inset: ; 24 | --tw-ring-offset-width: 0px; 25 | --tw-ring-offset-color: #fff; 26 | --tw-ring-color: rgb(59 130 246 / 0.5); 27 | --tw-ring-offset-shadow: 0 0 #0000; 28 | --tw-ring-shadow: 0 0 #0000; 29 | --tw-shadow: 0 0 #0000; 30 | --tw-shadow-colored: 0 0 #0000; 31 | --tw-blur: ; 32 | --tw-brightness: ; 33 | --tw-contrast: ; 34 | --tw-grayscale: ; 35 | --tw-hue-rotate: ; 36 | --tw-invert: ; 37 | --tw-saturate: ; 38 | --tw-sepia: ; 39 | --tw-drop-shadow: ; 40 | --tw-backdrop-blur: ; 41 | --tw-backdrop-brightness: ; 42 | --tw-backdrop-contrast: ; 43 | --tw-backdrop-grayscale: ; 44 | --tw-backdrop-hue-rotate: ; 45 | --tw-backdrop-invert: ; 46 | --tw-backdrop-opacity: ; 47 | --tw-backdrop-saturate: ; 48 | --tw-backdrop-sepia: ; 49 | --tw-contain-size: ; 50 | --tw-contain-layout: ; 51 | --tw-contain-paint: ; 52 | --tw-contain-style: ; 53 | } 54 | 55 | ::backdrop { 56 | --tw-border-spacing-x: 0; 57 | --tw-border-spacing-y: 0; 58 | --tw-translate-x: 0; 59 | --tw-translate-y: 0; 60 | --tw-rotate: 0; 61 | --tw-skew-x: 0; 62 | --tw-skew-y: 0; 63 | --tw-scale-x: 1; 64 | --tw-scale-y: 1; 65 | --tw-pan-x: ; 66 | --tw-pan-y: ; 67 | --tw-pinch-zoom: ; 68 | --tw-scroll-snap-strictness: proximity; 69 | --tw-gradient-from-position: ; 70 | --tw-gradient-via-position: ; 71 | --tw-gradient-to-position: ; 72 | --tw-ordinal: ; 73 | --tw-slashed-zero: ; 74 | --tw-numeric-figure: ; 75 | --tw-numeric-spacing: ; 76 | --tw-numeric-fraction: ; 77 | --tw-ring-inset: ; 78 | --tw-ring-offset-width: 0px; 79 | --tw-ring-offset-color: #fff; 80 | --tw-ring-color: rgb(59 130 246 / 0.5); 81 | --tw-ring-offset-shadow: 0 0 #0000; 82 | --tw-ring-shadow: 0 0 #0000; 83 | --tw-shadow: 0 0 #0000; 84 | --tw-shadow-colored: 0 0 #0000; 85 | --tw-blur: ; 86 | --tw-brightness: ; 87 | --tw-contrast: ; 88 | --tw-grayscale: ; 89 | --tw-hue-rotate: ; 90 | --tw-invert: ; 91 | --tw-saturate: ; 92 | --tw-sepia: ; 93 | --tw-drop-shadow: ; 94 | --tw-backdrop-blur: ; 95 | --tw-backdrop-brightness: ; 96 | --tw-backdrop-contrast: ; 97 | --tw-backdrop-grayscale: ; 98 | --tw-backdrop-hue-rotate: ; 99 | --tw-backdrop-invert: ; 100 | --tw-backdrop-opacity: ; 101 | --tw-backdrop-saturate: ; 102 | --tw-backdrop-sepia: ; 103 | --tw-contain-size: ; 104 | --tw-contain-layout: ; 105 | --tw-contain-paint: ; 106 | --tw-contain-style: ; 107 | } 108 | 109 | /* 110 | ! tailwindcss v3.4.16 | MIT License | https://tailwindcss.com 111 | */ 112 | 113 | /* 114 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 115 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 116 | */ 117 | 118 | *, 119 | ::before, 120 | ::after { 121 | box-sizing: border-box; 122 | /* 1 */ 123 | border-width: 0; 124 | /* 2 */ 125 | border-style: solid; 126 | /* 2 */ 127 | border-color: #e5e7eb; 128 | /* 2 */ 129 | } 130 | 131 | ::before, 132 | ::after { 133 | --tw-content: ''; 134 | } 135 | 136 | /* 137 | 1. Use a consistent sensible line-height in all browsers. 138 | 2. Prevent adjustments of font size after orientation changes in iOS. 139 | 3. Use a more readable tab size. 140 | 4. Use the user's configured `sans` font-family by default. 141 | 5. Use the user's configured `sans` font-feature-settings by default. 142 | 6. Use the user's configured `sans` font-variation-settings by default. 143 | 7. Disable tap highlights on iOS 144 | */ 145 | 146 | html, 147 | :host { 148 | line-height: 1.5; 149 | /* 1 */ 150 | -webkit-text-size-adjust: 100%; 151 | /* 2 */ 152 | -moz-tab-size: 4; 153 | /* 3 */ 154 | -o-tab-size: 4; 155 | tab-size: 4; 156 | /* 3 */ 157 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 158 | /* 4 */ 159 | font-feature-settings: normal; 160 | /* 5 */ 161 | font-variation-settings: normal; 162 | /* 6 */ 163 | -webkit-tap-highlight-color: transparent; 164 | /* 7 */ 165 | } 166 | 167 | /* 168 | 1. Remove the margin in all browsers. 169 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 170 | */ 171 | 172 | body { 173 | margin: 0; 174 | /* 1 */ 175 | line-height: inherit; 176 | /* 2 */ 177 | } 178 | 179 | /* 180 | 1. Add the correct height in Firefox. 181 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 182 | 3. Ensure horizontal rules are visible by default. 183 | */ 184 | 185 | hr { 186 | height: 0; 187 | /* 1 */ 188 | color: inherit; 189 | /* 2 */ 190 | border-top-width: 1px; 191 | /* 3 */ 192 | } 193 | 194 | /* 195 | Add the correct text decoration in Chrome, Edge, and Safari. 196 | */ 197 | 198 | abbr:where([title]) { 199 | -webkit-text-decoration: underline dotted; 200 | text-decoration: underline dotted; 201 | } 202 | 203 | /* 204 | Remove the default font size and weight for headings. 205 | */ 206 | 207 | h1, 208 | h2, 209 | h3, 210 | h4, 211 | h5, 212 | h6 { 213 | font-size: inherit; 214 | font-weight: inherit; 215 | } 216 | 217 | /* 218 | Reset links to optimize for opt-in styling instead of opt-out. 219 | */ 220 | 221 | a { 222 | color: inherit; 223 | text-decoration: inherit; 224 | } 225 | 226 | /* 227 | Add the correct font weight in Edge and Safari. 228 | */ 229 | 230 | b, 231 | strong { 232 | font-weight: bolder; 233 | } 234 | 235 | /* 236 | 1. Use the user's configured `mono` font-family by default. 237 | 2. Use the user's configured `mono` font-feature-settings by default. 238 | 3. Use the user's configured `mono` font-variation-settings by default. 239 | 4. Correct the odd `em` font sizing in all browsers. 240 | */ 241 | 242 | code, 243 | kbd, 244 | samp, 245 | pre { 246 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 247 | /* 1 */ 248 | font-feature-settings: normal; 249 | /* 2 */ 250 | font-variation-settings: normal; 251 | /* 3 */ 252 | font-size: 1em; 253 | /* 4 */ 254 | } 255 | 256 | /* 257 | Add the correct font size in all browsers. 258 | */ 259 | 260 | small { 261 | font-size: 80%; 262 | } 263 | 264 | /* 265 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 266 | */ 267 | 268 | sub, 269 | sup { 270 | font-size: 75%; 271 | line-height: 0; 272 | position: relative; 273 | vertical-align: baseline; 274 | } 275 | 276 | sub { 277 | bottom: -0.25em; 278 | } 279 | 280 | sup { 281 | top: -0.5em; 282 | } 283 | 284 | /* 285 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 286 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 287 | 3. Remove gaps between table borders by default. 288 | */ 289 | 290 | table { 291 | text-indent: 0; 292 | /* 1 */ 293 | border-color: inherit; 294 | /* 2 */ 295 | border-collapse: collapse; 296 | /* 3 */ 297 | } 298 | 299 | /* 300 | 1. Change the font styles in all browsers. 301 | 2. Remove the margin in Firefox and Safari. 302 | 3. Remove default padding in all browsers. 303 | */ 304 | 305 | button, 306 | input, 307 | optgroup, 308 | select, 309 | textarea { 310 | font-family: inherit; 311 | /* 1 */ 312 | font-feature-settings: inherit; 313 | /* 1 */ 314 | font-variation-settings: inherit; 315 | /* 1 */ 316 | font-size: 100%; 317 | /* 1 */ 318 | font-weight: inherit; 319 | /* 1 */ 320 | line-height: inherit; 321 | /* 1 */ 322 | letter-spacing: inherit; 323 | /* 1 */ 324 | color: inherit; 325 | /* 1 */ 326 | margin: 0; 327 | /* 2 */ 328 | padding: 0; 329 | /* 3 */ 330 | } 331 | 332 | /* 333 | Remove the inheritance of text transform in Edge and Firefox. 334 | */ 335 | 336 | button, 337 | select { 338 | text-transform: none; 339 | } 340 | 341 | /* 342 | 1. Correct the inability to style clickable types in iOS and Safari. 343 | 2. Remove default button styles. 344 | */ 345 | 346 | button, 347 | input:where([type='button']), 348 | input:where([type='reset']), 349 | input:where([type='submit']) { 350 | -webkit-appearance: button; 351 | /* 1 */ 352 | background-color: transparent; 353 | /* 2 */ 354 | background-image: none; 355 | /* 2 */ 356 | } 357 | 358 | /* 359 | Use the modern Firefox focus style for all focusable elements. 360 | */ 361 | 362 | :-moz-focusring { 363 | outline: auto; 364 | } 365 | 366 | /* 367 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 368 | */ 369 | 370 | :-moz-ui-invalid { 371 | box-shadow: none; 372 | } 373 | 374 | /* 375 | Add the correct vertical alignment in Chrome and Firefox. 376 | */ 377 | 378 | progress { 379 | vertical-align: baseline; 380 | } 381 | 382 | /* 383 | Correct the cursor style of increment and decrement buttons in Safari. 384 | */ 385 | 386 | ::-webkit-inner-spin-button, 387 | ::-webkit-outer-spin-button { 388 | height: auto; 389 | } 390 | 391 | /* 392 | 1. Correct the odd appearance in Chrome and Safari. 393 | 2. Correct the outline style in Safari. 394 | */ 395 | 396 | [type='search'] { 397 | -webkit-appearance: textfield; 398 | /* 1 */ 399 | outline-offset: -2px; 400 | /* 2 */ 401 | } 402 | 403 | /* 404 | Remove the inner padding in Chrome and Safari on macOS. 405 | */ 406 | 407 | ::-webkit-search-decoration { 408 | -webkit-appearance: none; 409 | } 410 | 411 | /* 412 | 1. Correct the inability to style clickable types in iOS and Safari. 413 | 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; 418 | /* 1 */ 419 | font: inherit; 420 | /* 2 */ 421 | } 422 | 423 | /* 424 | Add the correct display in Chrome and Safari. 425 | */ 426 | 427 | summary { 428 | display: list-item; 429 | } 430 | 431 | /* 432 | Removes the default spacing and border for appropriate elements. 433 | */ 434 | 435 | blockquote, 436 | dl, 437 | dd, 438 | h1, 439 | h2, 440 | h3, 441 | h4, 442 | h5, 443 | h6, 444 | hr, 445 | figure, 446 | p, 447 | pre { 448 | margin: 0; 449 | } 450 | 451 | fieldset { 452 | margin: 0; 453 | padding: 0; 454 | } 455 | 456 | legend { 457 | padding: 0; 458 | } 459 | 460 | ol, 461 | ul, 462 | menu { 463 | list-style: none; 464 | margin: 0; 465 | padding: 0; 466 | } 467 | 468 | /* 469 | Reset default styling for dialogs. 470 | */ 471 | 472 | dialog { 473 | padding: 0; 474 | } 475 | 476 | /* 477 | Prevent resizing textareas horizontally by default. 478 | */ 479 | 480 | textarea { 481 | resize: vertical; 482 | } 483 | 484 | /* 485 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 486 | 2. Set the default placeholder color to the user's configured gray 400 color. 487 | */ 488 | 489 | input::-moz-placeholder, textarea::-moz-placeholder { 490 | opacity: 1; 491 | /* 1 */ 492 | color: #9ca3af; 493 | /* 2 */ 494 | } 495 | 496 | input::placeholder, 497 | textarea::placeholder { 498 | opacity: 1; 499 | /* 1 */ 500 | color: #9ca3af; 501 | /* 2 */ 502 | } 503 | 504 | /* 505 | Set the default cursor for buttons. 506 | */ 507 | 508 | button, 509 | [role="button"] { 510 | cursor: pointer; 511 | } 512 | 513 | /* 514 | Make sure disabled buttons don't get the pointer cursor. 515 | */ 516 | 517 | :disabled { 518 | cursor: default; 519 | } 520 | 521 | /* 522 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 523 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 524 | This can trigger a poorly considered lint error in some tools but is included by design. 525 | */ 526 | 527 | img, 528 | svg, 529 | video, 530 | canvas, 531 | audio, 532 | iframe, 533 | embed, 534 | object { 535 | display: block; 536 | /* 1 */ 537 | vertical-align: middle; 538 | /* 2 */ 539 | } 540 | 541 | /* 542 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 543 | */ 544 | 545 | img, 546 | video { 547 | max-width: 100%; 548 | height: auto; 549 | } 550 | 551 | /* Make elements with the HTML hidden attribute stay hidden by default */ 552 | 553 | [hidden]:where(:not([hidden="until-found"])) { 554 | display: none; 555 | } 556 | 557 | .fixed { 558 | position: fixed; 559 | } 560 | 561 | .absolute { 562 | position: absolute; 563 | } 564 | 565 | .relative { 566 | position: relative; 567 | } 568 | 569 | .inset-1 { 570 | inset: 0.25rem; 571 | } 572 | 573 | .bottom-0 { 574 | bottom: 0px; 575 | } 576 | 577 | .left-0 { 578 | left: 0px; 579 | } 580 | 581 | .right-0 { 582 | right: 0px; 583 | } 584 | 585 | .top-0 { 586 | top: 0px; 587 | } 588 | 589 | .z-\[1000\] { 590 | z-index: 1000; 591 | } 592 | 593 | .z-\[1\] { 594 | z-index: 1; 595 | } 596 | 597 | .z-\[2\] { 598 | z-index: 2; 599 | } 600 | 601 | .m-0\.5 { 602 | margin: 0.125rem; 603 | } 604 | 605 | .m-3 { 606 | margin: 0.75rem; 607 | } 608 | 609 | .my-2 { 610 | margin-top: 0.5rem; 611 | margin-bottom: 0.5rem; 612 | } 613 | 614 | .mb-2 { 615 | margin-bottom: 0.5rem; 616 | } 617 | 618 | .mb-\[1px\] { 619 | margin-bottom: 1px; 620 | } 621 | 622 | .ml-12 { 623 | margin-left: 3rem; 624 | } 625 | 626 | .mr-0\.5 { 627 | margin-right: 0.125rem; 628 | } 629 | 630 | .mr-1 { 631 | margin-right: 0.25rem; 632 | } 633 | 634 | .inline-block { 635 | display: inline-block; 636 | } 637 | 638 | .inline { 639 | display: inline; 640 | } 641 | 642 | .flex { 643 | display: flex; 644 | } 645 | 646 | .inline-flex { 647 | display: inline-flex; 648 | } 649 | 650 | .table { 651 | display: table; 652 | } 653 | 654 | .table-cell { 655 | display: table-cell; 656 | } 657 | 658 | .grid { 659 | display: grid; 660 | } 661 | 662 | .h-4 { 663 | height: 1rem; 664 | } 665 | 666 | .h-6 { 667 | height: 1.5rem; 668 | } 669 | 670 | .h-64 { 671 | height: 16rem; 672 | } 673 | 674 | .h-8 { 675 | height: 2rem; 676 | } 677 | 678 | .h-\[110px\] { 679 | height: 110px; 680 | } 681 | 682 | .h-\[140px\] { 683 | height: 140px; 684 | } 685 | 686 | .h-\[48px\] { 687 | height: 48px; 688 | } 689 | 690 | .h-\[96px\] { 691 | height: 96px; 692 | } 693 | 694 | .h-full { 695 | height: 100%; 696 | } 697 | 698 | .max-h-\[400px\] { 699 | max-height: 400px; 700 | } 701 | 702 | .w-64 { 703 | width: 16rem; 704 | } 705 | 706 | .w-8 { 707 | width: 2rem; 708 | } 709 | 710 | .w-\[100px\] { 711 | width: 100px; 712 | } 713 | 714 | .w-\[64px\] { 715 | width: 64px; 716 | } 717 | 718 | .w-\[80px\] { 719 | width: 80px; 720 | } 721 | 722 | .w-\[96px\] { 723 | width: 96px; 724 | } 725 | 726 | .w-full { 727 | width: 100%; 728 | } 729 | 730 | .min-w-32 { 731 | min-width: 8rem; 732 | } 733 | 734 | .min-w-8 { 735 | min-width: 2rem; 736 | } 737 | 738 | .max-w-64 { 739 | max-width: 16rem; 740 | } 741 | 742 | .flex-grow { 743 | flex-grow: 1; 744 | } 745 | 746 | .translate-y-\[4px\] { 747 | --tw-translate-y: 4px; 748 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 749 | } 750 | 751 | .cursor-pointer { 752 | cursor: pointer; 753 | } 754 | 755 | .flex-col { 756 | flex-direction: column; 757 | } 758 | 759 | .flex-wrap { 760 | flex-wrap: wrap; 761 | } 762 | 763 | .content-start { 764 | align-content: flex-start; 765 | } 766 | 767 | .items-center { 768 | align-items: center; 769 | } 770 | 771 | .justify-center { 772 | justify-content: center; 773 | } 774 | 775 | .justify-between { 776 | justify-content: space-between; 777 | } 778 | 779 | .gap-0\.5 { 780 | gap: 0.125rem; 781 | } 782 | 783 | .gap-1 { 784 | gap: 0.25rem; 785 | } 786 | 787 | .gap-2 { 788 | gap: 0.5rem; 789 | } 790 | 791 | .overflow-auto { 792 | overflow: auto; 793 | } 794 | 795 | .overflow-hidden { 796 | overflow: hidden; 797 | } 798 | 799 | .overflow-y-scroll { 800 | overflow-y: scroll; 801 | } 802 | 803 | .rounded { 804 | border-radius: 0.25rem; 805 | } 806 | 807 | .rounded-tl { 808 | border-top-left-radius: 0.25rem; 809 | } 810 | 811 | .rounded-tr { 812 | border-top-right-radius: 0.25rem; 813 | } 814 | 815 | .border-b-2 { 816 | border-bottom-width: 2px; 817 | } 818 | 819 | .border-blackDark { 820 | --tw-border-opacity: 1; 821 | border-color: rgb(36 35 36 / var(--tw-border-opacity, 1)); 822 | } 823 | 824 | .border-blackLight { 825 | --tw-border-opacity: 1; 826 | border-color: rgb(64 63 64 / var(--tw-border-opacity, 1)); 827 | } 828 | 829 | .bg-\[\#00c659\] { 830 | --tw-bg-opacity: 1; 831 | background-color: rgb(0 198 89 / var(--tw-bg-opacity, 1)); 832 | } 833 | 834 | .bg-\[\#01711f\] { 835 | --tw-bg-opacity: 1; 836 | background-color: rgb(1 113 31 / var(--tw-bg-opacity, 1)); 837 | } 838 | 839 | .bg-\[\#024d07\] { 840 | --tw-bg-opacity: 1; 841 | background-color: rgb(2 77 7 / var(--tw-bg-opacity, 1)); 842 | } 843 | 844 | .bg-blackDark { 845 | --tw-bg-opacity: 1; 846 | background-color: rgb(36 35 36 / var(--tw-bg-opacity, 1)); 847 | } 848 | 849 | .bg-blackLight { 850 | --tw-bg-opacity: 1; 851 | background-color: rgb(64 63 64 / var(--tw-bg-opacity, 1)); 852 | } 853 | 854 | .bg-blackMedium { 855 | --tw-bg-opacity: 1; 856 | background-color: rgb(49 48 49 / var(--tw-bg-opacity, 1)); 857 | } 858 | 859 | .bg-grayLight { 860 | --tw-bg-opacity: 1; 861 | background-color: rgb(142 142 142 / var(--tw-bg-opacity, 1)); 862 | } 863 | 864 | .bg-orangeDark { 865 | --tw-bg-opacity: 1; 866 | background-color: rgb(255 171 0 / var(--tw-bg-opacity, 1)); 867 | } 868 | 869 | .bg-orangeLight { 870 | --tw-bg-opacity: 1; 871 | background-color: rgb(241 190 100 / var(--tw-bg-opacity, 1)); 872 | } 873 | 874 | .bg-textBeige { 875 | --tw-bg-opacity: 1; 876 | background-color: rgb(232 210 176 / var(--tw-bg-opacity, 1)); 877 | } 878 | 879 | .bg-textBlue { 880 | --tw-bg-opacity: 1; 881 | background-color: rgb(119 188 218 / var(--tw-bg-opacity, 1)); 882 | } 883 | 884 | .bg-textGreen { 885 | --tw-bg-opacity: 1; 886 | background-color: rgb(41 155 55 / var(--tw-bg-opacity, 1)); 887 | } 888 | 889 | .bg-textYellow { 890 | --tw-bg-opacity: 1; 891 | background-color: rgb(221 184 67 / var(--tw-bg-opacity, 1)); 892 | } 893 | 894 | .\!p-0 { 895 | padding: 0px !important; 896 | } 897 | 898 | .p-0\.5 { 899 | padding: 0.125rem; 900 | } 901 | 902 | .p-1 { 903 | padding: 0.25rem; 904 | } 905 | 906 | .p-2 { 907 | padding: 0.5rem; 908 | } 909 | 910 | .p-3 { 911 | padding: 0.75rem; 912 | } 913 | 914 | .px-1 { 915 | padding-left: 0.25rem; 916 | padding-right: 0.25rem; 917 | } 918 | 919 | .px-2 { 920 | padding-left: 0.5rem; 921 | padding-right: 0.5rem; 922 | } 923 | 924 | .px-4 { 925 | padding-left: 1rem; 926 | padding-right: 1rem; 927 | } 928 | 929 | .py-0\.5 { 930 | padding-top: 0.125rem; 931 | padding-bottom: 0.125rem; 932 | } 933 | 934 | .py-1 { 935 | padding-top: 0.25rem; 936 | padding-bottom: 0.25rem; 937 | } 938 | 939 | .py-2 { 940 | padding-top: 0.5rem; 941 | padding-bottom: 0.5rem; 942 | } 943 | 944 | .py-4 { 945 | padding-top: 1rem; 946 | padding-bottom: 1rem; 947 | } 948 | 949 | .pb-2 { 950 | padding-bottom: 0.5rem; 951 | } 952 | 953 | .pt-2 { 954 | padding-top: 0.5rem; 955 | } 956 | 957 | .text-right { 958 | text-align: right; 959 | } 960 | 961 | .text-sm { 962 | font-size: 0.875rem; 963 | line-height: 1.25rem; 964 | } 965 | 966 | .text-xs { 967 | font-size: 0.75rem; 968 | line-height: 1rem; 969 | } 970 | 971 | .font-bold { 972 | font-weight: 700; 973 | } 974 | 975 | .\!text-black { 976 | --tw-text-opacity: 1 !important; 977 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)) !important; 978 | } 979 | 980 | .text-black { 981 | --tw-text-opacity: 1; 982 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)); 983 | } 984 | 985 | .text-orangeDark { 986 | --tw-text-opacity: 1; 987 | color: rgb(255 171 0 / var(--tw-text-opacity, 1)); 988 | } 989 | 990 | .text-textBeige { 991 | --tw-text-opacity: 1; 992 | color: rgb(232 210 176 / var(--tw-text-opacity, 1)); 993 | } 994 | 995 | .text-textBlue { 996 | --tw-text-opacity: 1; 997 | color: rgb(119 188 218 / var(--tw-text-opacity, 1)); 998 | } 999 | 1000 | .text-textYellow { 1001 | --tw-text-opacity: 1; 1002 | color: rgb(221 184 67 / var(--tw-text-opacity, 1)); 1003 | } 1004 | 1005 | .text-white { 1006 | --tw-text-opacity: 1; 1007 | color: rgb(255 255 255 / var(--tw-text-opacity, 1)); 1008 | } 1009 | 1010 | .shadow { 1011 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 1012 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); 1013 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1014 | } 1015 | 1016 | .shadow-btn-large { 1017 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.5);; 1018 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1019 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1020 | } 1021 | 1022 | .shadow-btn-small { 1023 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 1px 2px -1px rgba(255,255,255,0.5);; 1024 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 2px -1px var(--tw-shadow-color); 1025 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1026 | } 1027 | 1028 | .shadow-btnz { 1029 | --tw-shadow: inset 0px -2px 7px 0px #000000, 0px 2px 4px 1px rgba(0,0,0,0.77), inset 0px 1px 4px -1px rgba(255,255,255,0.8);; 1030 | --tw-shadow-colored: inset 0px -2px 7px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 4px -1px var(--tw-shadow-color); 1031 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1032 | } 1033 | 1034 | .shadow-deepinset { 1035 | --tw-shadow: inset 0px 2px 4px 2px rgba(0,0,0,1);; 1036 | --tw-shadow-colored: inset 0px 2px 4px 2px var(--tw-shadow-color); 1037 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1038 | } 1039 | 1040 | .shadow-grid-backdrop { 1041 | --tw-shadow: inset 0px 1px 1px -1px rgba(255,255,255,0.5), 0px 1px 1px 1px rgba(0,0,0,0.2);; 1042 | --tw-shadow-colored: inset 0px 1px 1px -1px var(--tw-shadow-color), 0px 1px 1px 1px var(--tw-shadow-color); 1043 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1044 | } 1045 | 1046 | .shadow-inset-1 { 1047 | --tw-shadow: inset 0px 1px 3px 1px rgba(0,0,0,.5); 1048 | --tw-shadow-colored: inset 0px 1px 3px 1px var(--tw-shadow-color); 1049 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1050 | } 1051 | 1052 | .shadow-orangeglow { 1053 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);; 1054 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 1px 3px 2px var(--tw-shadow-color), inset 0px 1px 2px 0px var(--tw-shadow-color); 1055 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1056 | } 1057 | 1058 | .shadow-technology { 1059 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1060 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1061 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1062 | } 1063 | 1064 | .shadow-topglow-1 { 1065 | --tw-shadow: inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1066 | --tw-shadow-colored: inset 0px 2px 2px -1px var(--tw-shadow-color); 1067 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1068 | } 1069 | 1070 | .shadow-topglow-2 { 1071 | --tw-shadow: inset 0px 1px 3px -1px rgba(255,255,255,0.5);; 1072 | --tw-shadow-colored: inset 0px 1px 3px -1px var(--tw-shadow-color); 1073 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1074 | } 1075 | 1076 | .filter { 1077 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 1078 | } 1079 | 1080 | .hover\:bg-blackDark:hover { 1081 | --tw-bg-opacity: 1; 1082 | background-color: rgb(36 35 36 / var(--tw-bg-opacity, 1)); 1083 | } 1084 | 1085 | .hover\:bg-blackLight:hover { 1086 | --tw-bg-opacity: 1; 1087 | background-color: rgb(64 63 64 / var(--tw-bg-opacity, 1)); 1088 | } 1089 | 1090 | .hover\:bg-blackMedium:hover { 1091 | --tw-bg-opacity: 1; 1092 | background-color: rgb(49 48 49 / var(--tw-bg-opacity, 1)); 1093 | } 1094 | 1095 | .hover\:bg-grayLight:hover { 1096 | --tw-bg-opacity: 1; 1097 | background-color: rgb(142 142 142 / var(--tw-bg-opacity, 1)); 1098 | } 1099 | 1100 | .hover\:bg-orangeDark:hover { 1101 | --tw-bg-opacity: 1; 1102 | background-color: rgb(255 171 0 / var(--tw-bg-opacity, 1)); 1103 | } 1104 | 1105 | .hover\:bg-orangeLight:hover { 1106 | --tw-bg-opacity: 1; 1107 | background-color: rgb(241 190 100 / var(--tw-bg-opacity, 1)); 1108 | } 1109 | 1110 | .hover\:bg-textBeige:hover { 1111 | --tw-bg-opacity: 1; 1112 | background-color: rgb(232 210 176 / var(--tw-bg-opacity, 1)); 1113 | } 1114 | 1115 | .hover\:bg-textBlue:hover { 1116 | --tw-bg-opacity: 1; 1117 | background-color: rgb(119 188 218 / var(--tw-bg-opacity, 1)); 1118 | } 1119 | 1120 | .hover\:bg-textGreen:hover { 1121 | --tw-bg-opacity: 1; 1122 | background-color: rgb(41 155 55 / var(--tw-bg-opacity, 1)); 1123 | } 1124 | 1125 | .hover\:bg-textYellow:hover { 1126 | --tw-bg-opacity: 1; 1127 | background-color: rgb(221 184 67 / var(--tw-bg-opacity, 1)); 1128 | } 1129 | 1130 | .hover\:text-black:hover { 1131 | --tw-text-opacity: 1; 1132 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)); 1133 | } 1134 | 1135 | .hover\:shadow-btn-large:hover { 1136 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.5);; 1137 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1138 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1139 | } 1140 | 1141 | .hover\:shadow-btn-small:hover { 1142 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 1px 2px -1px rgba(255,255,255,0.5);; 1143 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 2px -1px var(--tw-shadow-color); 1144 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1145 | } 1146 | 1147 | .hover\:shadow-btnz:hover { 1148 | --tw-shadow: inset 0px -2px 7px 0px #000000, 0px 2px 4px 1px rgba(0,0,0,0.77), inset 0px 1px 4px -1px rgba(255,255,255,0.8);; 1149 | --tw-shadow-colored: inset 0px -2px 7px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 4px -1px var(--tw-shadow-color); 1150 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1151 | } 1152 | 1153 | .hover\:shadow-deepinset:hover { 1154 | --tw-shadow: inset 0px 2px 4px 2px rgba(0,0,0,1);; 1155 | --tw-shadow-colored: inset 0px 2px 4px 2px var(--tw-shadow-color); 1156 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1157 | } 1158 | 1159 | .hover\:shadow-grid-backdrop:hover { 1160 | --tw-shadow: inset 0px 1px 1px -1px rgba(255,255,255,0.5), 0px 1px 1px 1px rgba(0,0,0,0.2);; 1161 | --tw-shadow-colored: inset 0px 1px 1px -1px var(--tw-shadow-color), 0px 1px 1px 1px var(--tw-shadow-color); 1162 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1163 | } 1164 | 1165 | .hover\:shadow-inset-1:hover { 1166 | --tw-shadow: inset 0px 1px 3px 1px rgba(0,0,0,.5); 1167 | --tw-shadow-colored: inset 0px 1px 3px 1px var(--tw-shadow-color); 1168 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1169 | } 1170 | 1171 | .hover\:shadow-orangeglow:hover { 1172 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);; 1173 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 1px 3px 2px var(--tw-shadow-color), inset 0px 1px 2px 0px var(--tw-shadow-color); 1174 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1175 | } 1176 | 1177 | .hover\:shadow-technology:hover { 1178 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1179 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1180 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1181 | } 1182 | 1183 | .hover\:shadow-topglow-1:hover { 1184 | --tw-shadow: inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1185 | --tw-shadow-colored: inset 0px 2px 2px -1px var(--tw-shadow-color); 1186 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1187 | } 1188 | 1189 | .hover\:shadow-topglow-2:hover { 1190 | --tw-shadow: inset 0px 1px 3px -1px rgba(255,255,255,0.5);; 1191 | --tw-shadow-colored: inset 0px 1px 3px -1px var(--tw-shadow-color); 1192 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1193 | } 1194 | 1195 | .active\:bg-blackDark:active { 1196 | --tw-bg-opacity: 1; 1197 | background-color: rgb(36 35 36 / var(--tw-bg-opacity, 1)); 1198 | } 1199 | 1200 | .active\:bg-blackLight:active { 1201 | --tw-bg-opacity: 1; 1202 | background-color: rgb(64 63 64 / var(--tw-bg-opacity, 1)); 1203 | } 1204 | 1205 | .active\:bg-blackMedium:active { 1206 | --tw-bg-opacity: 1; 1207 | background-color: rgb(49 48 49 / var(--tw-bg-opacity, 1)); 1208 | } 1209 | 1210 | .active\:bg-grayLight:active { 1211 | --tw-bg-opacity: 1; 1212 | background-color: rgb(142 142 142 / var(--tw-bg-opacity, 1)); 1213 | } 1214 | 1215 | .active\:bg-orangeDark:active { 1216 | --tw-bg-opacity: 1; 1217 | background-color: rgb(255 171 0 / var(--tw-bg-opacity, 1)); 1218 | } 1219 | 1220 | .active\:bg-orangeLight:active { 1221 | --tw-bg-opacity: 1; 1222 | background-color: rgb(241 190 100 / var(--tw-bg-opacity, 1)); 1223 | } 1224 | 1225 | .active\:bg-textBeige:active { 1226 | --tw-bg-opacity: 1; 1227 | background-color: rgb(232 210 176 / var(--tw-bg-opacity, 1)); 1228 | } 1229 | 1230 | .active\:bg-textBlue:active { 1231 | --tw-bg-opacity: 1; 1232 | background-color: rgb(119 188 218 / var(--tw-bg-opacity, 1)); 1233 | } 1234 | 1235 | .active\:bg-textGreen:active { 1236 | --tw-bg-opacity: 1; 1237 | background-color: rgb(41 155 55 / var(--tw-bg-opacity, 1)); 1238 | } 1239 | 1240 | .active\:bg-textYellow:active { 1241 | --tw-bg-opacity: 1; 1242 | background-color: rgb(221 184 67 / var(--tw-bg-opacity, 1)); 1243 | } 1244 | 1245 | .active\:shadow-btn-large:active { 1246 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.5);; 1247 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1248 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1249 | } 1250 | 1251 | .active\:shadow-btn-small:active { 1252 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 1px 2px -1px rgba(255,255,255,0.5);; 1253 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 2px -1px var(--tw-shadow-color); 1254 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1255 | } 1256 | 1257 | .active\:shadow-btnz:active { 1258 | --tw-shadow: inset 0px -2px 7px 0px #000000, 0px 2px 4px 1px rgba(0,0,0,0.77), inset 0px 1px 4px -1px rgba(255,255,255,0.8);; 1259 | --tw-shadow-colored: inset 0px -2px 7px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 4px -1px var(--tw-shadow-color); 1260 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1261 | } 1262 | 1263 | .active\:shadow-deepinset:active { 1264 | --tw-shadow: inset 0px 2px 4px 2px rgba(0,0,0,1);; 1265 | --tw-shadow-colored: inset 0px 2px 4px 2px var(--tw-shadow-color); 1266 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1267 | } 1268 | 1269 | .active\:shadow-grid-backdrop:active { 1270 | --tw-shadow: inset 0px 1px 1px -1px rgba(255,255,255,0.5), 0px 1px 1px 1px rgba(0,0,0,0.2);; 1271 | --tw-shadow-colored: inset 0px 1px 1px -1px var(--tw-shadow-color), 0px 1px 1px 1px var(--tw-shadow-color); 1272 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1273 | } 1274 | 1275 | .active\:shadow-inset-1:active { 1276 | --tw-shadow: inset 0px 1px 3px 1px rgba(0,0,0,.5); 1277 | --tw-shadow-colored: inset 0px 1px 3px 1px var(--tw-shadow-color); 1278 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1279 | } 1280 | 1281 | .active\:shadow-orangeglow:active { 1282 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);; 1283 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 1px 3px 2px var(--tw-shadow-color), inset 0px 1px 2px 0px var(--tw-shadow-color); 1284 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1285 | } 1286 | 1287 | .active\:shadow-technology:active { 1288 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1289 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1290 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1291 | } 1292 | 1293 | .active\:shadow-topglow-1:active { 1294 | --tw-shadow: inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1295 | --tw-shadow-colored: inset 0px 2px 2px -1px var(--tw-shadow-color); 1296 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1297 | } 1298 | 1299 | .active\:shadow-topglow-2:active { 1300 | --tw-shadow: inset 0px 1px 3px -1px rgba(255,255,255,0.5);; 1301 | --tw-shadow-colored: inset 0px 1px 3px -1px var(--tw-shadow-color); 1302 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1303 | } 1304 | 1305 | .group:hover .group-hover\:bg-\[\#04db65\] { 1306 | --tw-bg-opacity: 1; 1307 | background-color: rgb(4 219 101 / var(--tw-bg-opacity, 1)); 1308 | } 1309 | 1310 | .group:hover .group-hover\:bg-blackDark { 1311 | --tw-bg-opacity: 1; 1312 | background-color: rgb(36 35 36 / var(--tw-bg-opacity, 1)); 1313 | } 1314 | 1315 | .group:hover .group-hover\:bg-blackLight { 1316 | --tw-bg-opacity: 1; 1317 | background-color: rgb(64 63 64 / var(--tw-bg-opacity, 1)); 1318 | } 1319 | 1320 | .group:hover .group-hover\:bg-blackMedium { 1321 | --tw-bg-opacity: 1; 1322 | background-color: rgb(49 48 49 / var(--tw-bg-opacity, 1)); 1323 | } 1324 | 1325 | .group:hover .group-hover\:bg-grayLight { 1326 | --tw-bg-opacity: 1; 1327 | background-color: rgb(142 142 142 / var(--tw-bg-opacity, 1)); 1328 | } 1329 | 1330 | .group:hover .group-hover\:bg-orangeDark { 1331 | --tw-bg-opacity: 1; 1332 | background-color: rgb(255 171 0 / var(--tw-bg-opacity, 1)); 1333 | } 1334 | 1335 | .group:hover .group-hover\:bg-orangeLight { 1336 | --tw-bg-opacity: 1; 1337 | background-color: rgb(241 190 100 / var(--tw-bg-opacity, 1)); 1338 | } 1339 | 1340 | .group:hover .group-hover\:bg-textBeige { 1341 | --tw-bg-opacity: 1; 1342 | background-color: rgb(232 210 176 / var(--tw-bg-opacity, 1)); 1343 | } 1344 | 1345 | .group:hover .group-hover\:bg-textBlue { 1346 | --tw-bg-opacity: 1; 1347 | background-color: rgb(119 188 218 / var(--tw-bg-opacity, 1)); 1348 | } 1349 | 1350 | .group:hover .group-hover\:bg-textGreen { 1351 | --tw-bg-opacity: 1; 1352 | background-color: rgb(41 155 55 / var(--tw-bg-opacity, 1)); 1353 | } 1354 | 1355 | .group:hover .group-hover\:bg-textYellow { 1356 | --tw-bg-opacity: 1; 1357 | background-color: rgb(221 184 67 / var(--tw-bg-opacity, 1)); 1358 | } 1359 | 1360 | .group:hover .group-hover\:text-black { 1361 | --tw-text-opacity: 1; 1362 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)); 1363 | } 1364 | 1365 | .group:hover .group-hover\:shadow-btn-large { 1366 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.5);; 1367 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1368 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1369 | } 1370 | 1371 | .group:hover .group-hover\:shadow-btn-small { 1372 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 1px 2px -1px rgba(255,255,255,0.5);; 1373 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 2px -1px var(--tw-shadow-color); 1374 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1375 | } 1376 | 1377 | .group:hover .group-hover\:shadow-btnz { 1378 | --tw-shadow: inset 0px -2px 7px 0px #000000, 0px 2px 4px 1px rgba(0,0,0,0.77), inset 0px 1px 4px -1px rgba(255,255,255,0.8);; 1379 | --tw-shadow-colored: inset 0px -2px 7px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 4px -1px var(--tw-shadow-color); 1380 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1381 | } 1382 | 1383 | .group:hover .group-hover\:shadow-deepinset { 1384 | --tw-shadow: inset 0px 2px 4px 2px rgba(0,0,0,1);; 1385 | --tw-shadow-colored: inset 0px 2px 4px 2px var(--tw-shadow-color); 1386 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1387 | } 1388 | 1389 | .group:hover .group-hover\:shadow-grid-backdrop { 1390 | --tw-shadow: inset 0px 1px 1px -1px rgba(255,255,255,0.5), 0px 1px 1px 1px rgba(0,0,0,0.2);; 1391 | --tw-shadow-colored: inset 0px 1px 1px -1px var(--tw-shadow-color), 0px 1px 1px 1px var(--tw-shadow-color); 1392 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1393 | } 1394 | 1395 | .group:hover .group-hover\:shadow-inset-1 { 1396 | --tw-shadow: inset 0px 1px 3px 1px rgba(0,0,0,.5); 1397 | --tw-shadow-colored: inset 0px 1px 3px 1px var(--tw-shadow-color); 1398 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1399 | } 1400 | 1401 | .group:hover .group-hover\:shadow-orangeglow { 1402 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);; 1403 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 1px 3px 2px var(--tw-shadow-color), inset 0px 1px 2px 0px var(--tw-shadow-color); 1404 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1405 | } 1406 | 1407 | .group:hover .group-hover\:shadow-technology { 1408 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1409 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1410 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1411 | } 1412 | 1413 | .group:hover .group-hover\:shadow-topglow-1 { 1414 | --tw-shadow: inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1415 | --tw-shadow-colored: inset 0px 2px 2px -1px var(--tw-shadow-color); 1416 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1417 | } 1418 | 1419 | .group:hover .group-hover\:shadow-topglow-2 { 1420 | --tw-shadow: inset 0px 1px 3px -1px rgba(255,255,255,0.5);; 1421 | --tw-shadow-colored: inset 0px 1px 3px -1px var(--tw-shadow-color); 1422 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1423 | } 1424 | 1425 | .group:active .group-active\:bg-\[\#03ad50\] { 1426 | --tw-bg-opacity: 1; 1427 | background-color: rgb(3 173 80 / var(--tw-bg-opacity, 1)); 1428 | } 1429 | 1430 | .group:active .group-active\:bg-blackDark { 1431 | --tw-bg-opacity: 1; 1432 | background-color: rgb(36 35 36 / var(--tw-bg-opacity, 1)); 1433 | } 1434 | 1435 | .group:active .group-active\:bg-blackLight { 1436 | --tw-bg-opacity: 1; 1437 | background-color: rgb(64 63 64 / var(--tw-bg-opacity, 1)); 1438 | } 1439 | 1440 | .group:active .group-active\:bg-blackMedium { 1441 | --tw-bg-opacity: 1; 1442 | background-color: rgb(49 48 49 / var(--tw-bg-opacity, 1)); 1443 | } 1444 | 1445 | .group:active .group-active\:bg-grayLight { 1446 | --tw-bg-opacity: 1; 1447 | background-color: rgb(142 142 142 / var(--tw-bg-opacity, 1)); 1448 | } 1449 | 1450 | .group:active .group-active\:bg-orangeDark { 1451 | --tw-bg-opacity: 1; 1452 | background-color: rgb(255 171 0 / var(--tw-bg-opacity, 1)); 1453 | } 1454 | 1455 | .group:active .group-active\:bg-orangeLight { 1456 | --tw-bg-opacity: 1; 1457 | background-color: rgb(241 190 100 / var(--tw-bg-opacity, 1)); 1458 | } 1459 | 1460 | .group:active .group-active\:bg-textBeige { 1461 | --tw-bg-opacity: 1; 1462 | background-color: rgb(232 210 176 / var(--tw-bg-opacity, 1)); 1463 | } 1464 | 1465 | .group:active .group-active\:bg-textBlue { 1466 | --tw-bg-opacity: 1; 1467 | background-color: rgb(119 188 218 / var(--tw-bg-opacity, 1)); 1468 | } 1469 | 1470 | .group:active .group-active\:bg-textGreen { 1471 | --tw-bg-opacity: 1; 1472 | background-color: rgb(41 155 55 / var(--tw-bg-opacity, 1)); 1473 | } 1474 | 1475 | .group:active .group-active\:bg-textYellow { 1476 | --tw-bg-opacity: 1; 1477 | background-color: rgb(221 184 67 / var(--tw-bg-opacity, 1)); 1478 | } 1479 | 1480 | .group:active .group-active\:shadow-btn-large { 1481 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.5);; 1482 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1483 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1484 | } 1485 | 1486 | .group:active .group-active\:shadow-btn-small { 1487 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 1px 2px -1px rgba(255,255,255,0.5);; 1488 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 2px -1px var(--tw-shadow-color); 1489 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1490 | } 1491 | 1492 | .group:active .group-active\:shadow-btnz { 1493 | --tw-shadow: inset 0px -2px 7px 0px #000000, 0px 2px 4px 1px rgba(0,0,0,0.77), inset 0px 1px 4px -1px rgba(255,255,255,0.8);; 1494 | --tw-shadow-colored: inset 0px -2px 7px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 1px 4px -1px var(--tw-shadow-color); 1495 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1496 | } 1497 | 1498 | .group:active .group-active\:shadow-deepinset { 1499 | --tw-shadow: inset 0px 2px 4px 2px rgba(0,0,0,1);; 1500 | --tw-shadow-colored: inset 0px 2px 4px 2px var(--tw-shadow-color); 1501 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1502 | } 1503 | 1504 | .group:active .group-active\:shadow-grid-backdrop { 1505 | --tw-shadow: inset 0px 1px 1px -1px rgba(255,255,255,0.5), 0px 1px 1px 1px rgba(0,0,0,0.2);; 1506 | --tw-shadow-colored: inset 0px 1px 1px -1px var(--tw-shadow-color), 0px 1px 1px 1px var(--tw-shadow-color); 1507 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1508 | } 1509 | 1510 | .group:active .group-active\:shadow-inset-1 { 1511 | --tw-shadow: inset 0px 1px 3px 1px rgba(0,0,0,.5); 1512 | --tw-shadow-colored: inset 0px 1px 3px 1px var(--tw-shadow-color); 1513 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1514 | } 1515 | 1516 | .group:active .group-active\:shadow-orangeglow { 1517 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);; 1518 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 1px 3px 2px var(--tw-shadow-color), inset 0px 1px 2px 0px var(--tw-shadow-color); 1519 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1520 | } 1521 | 1522 | .group:active .group-active\:shadow-technology { 1523 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 2px 4px 1px rgba(0,0,0,0.3), inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1524 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 2px 4px 1px var(--tw-shadow-color), inset 0px 2px 2px -1px var(--tw-shadow-color); 1525 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1526 | } 1527 | 1528 | .group:active .group-active\:shadow-topglow-1 { 1529 | --tw-shadow: inset 0px 2px 2px -1px rgba(255,255,255,0.7);; 1530 | --tw-shadow-colored: inset 0px 2px 2px -1px var(--tw-shadow-color); 1531 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1532 | } 1533 | 1534 | .group:active .group-active\:shadow-topglow-2 { 1535 | --tw-shadow: inset 0px 1px 3px -1px rgba(255,255,255,0.5);; 1536 | --tw-shadow-colored: inset 0px 1px 3px -1px var(--tw-shadow-color); 1537 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1538 | } 1539 | 1540 | .data-\[state\=active\]\:translate-y-\[4px\][data-state="active"] { 1541 | --tw-translate-y: 4px; 1542 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 1543 | } 1544 | 1545 | .data-\[state\=active\]\:bg-blackLight[data-state="active"] { 1546 | --tw-bg-opacity: 1; 1547 | background-color: rgb(64 63 64 / var(--tw-bg-opacity, 1)); 1548 | } 1549 | 1550 | .data-\[state\=active\]\:text-textBeige[data-state="active"] { 1551 | --tw-text-opacity: 1; 1552 | color: rgb(232 210 176 / var(--tw-text-opacity, 1)); 1553 | } 1554 | 1555 | .data-\[state\=inactive\]\:hover\:shadow-orangeglow:hover[data-state="inactive"] { 1556 | --tw-shadow: inset 0px -2px 3px 0px rgba(0,0,0,0.7), 0px 1px 3px 2px rgba(255,171,0,0.3), inset 0px 1px 2px 0px rgba(255,255,255,0.8);; 1557 | --tw-shadow-colored: inset 0px -2px 3px 0px var(--tw-shadow-color), 0px 1px 3px 2px var(--tw-shadow-color), inset 0px 1px 2px 0px var(--tw-shadow-color); 1558 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1559 | } --------------------------------------------------------------------------------