Choose your preferred toy mount options. You can select multiple options from different categories.
@@ -190,8 +190,8 @@ export default function ToyMountStep({ config, updateConfig }) {
)}
{!hasSelection && (
-
-
+
+
Note: At least one toy mount option is recommended for your build.
diff --git a/website/src/contexts/ThemeContext.jsx b/website/src/contexts/ThemeContext.jsx
new file mode 100644
index 0000000..46ec9f3
--- /dev/null
+++ b/website/src/contexts/ThemeContext.jsx
@@ -0,0 +1,75 @@
+import { createContext, useContext, useState, useEffect } from 'react';
+
+const ThemeContext = createContext();
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+};
+
+export const ThemeProvider = ({ children }) => {
+ const [theme, setTheme] = useState(() => {
+ // Check localStorage first
+ if (typeof window !== 'undefined') {
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme === 'light' || savedTheme === 'dark') {
+ return savedTheme;
+ }
+ // Check system preference only if no saved preference
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ return 'dark';
+ }
+ }
+ return 'light';
+ });
+
+ useEffect(() => {
+ // Apply theme to document
+ if (typeof document !== 'undefined') {
+ if (theme === 'dark') {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ }
+ // Save to localStorage
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('theme', theme);
+ }
+ }, [theme]);
+
+ // Listen for system theme changes
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const handleChange = (e) => {
+ // Only update if user hasn't manually set a preference
+ // If theme is saved in localStorage, it means user has manually set it
+ const savedTheme = localStorage.getItem('theme');
+ if (!savedTheme || savedTheme === 'null' || savedTheme === 'undefined') {
+ setTheme(e.matches ? 'dark' : 'light');
+ }
+ };
+ mediaQuery.addEventListener('change', handleChange);
+ return () => mediaQuery.removeEventListener('change', handleChange);
+ }, []);
+
+ const toggleTheme = () => {
+ setTheme((prevTheme) => {
+ const newTheme = prevTheme === 'light' ? 'dark' : 'light';
+ // Immediately save to localStorage to prevent system preference from overriding
+ localStorage.setItem('theme', newTheme);
+ return newTheme;
+ });
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/website/src/data/components/pcb.json b/website/src/data/components/pcb.json
new file mode 100644
index 0000000..cad2501
--- /dev/null
+++ b/website/src/data/components/pcb.json
@@ -0,0 +1,75 @@
+{
+ "3030-mount": {
+ "category": "PCB Mount",
+ "type": "base",
+ "printedParts": [
+ {
+ "id": "ossm-pcb-3030-mount",
+ "name": "PCB 3030 Mount",
+ "description": "PCB mount for 3030 extrusion",
+ "filamentEstimate": 15,
+ "timeEstimate": "45m",
+ "colour": "primary",
+ "required": true,
+ "filePath": "OSSM - PCB - 3030 Mount.stl",
+ "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/PCB/OSSM%20-%20PCB%20-%203030%20Mount.stl?raw=true"
+ },
+ {
+ "id": "ossm-pcb-3030-mount-cover",
+ "name": "PCB 3030 Mount Cover",
+ "description": "Cover for the 3030 mount",
+ "filamentEstimate": 15,
+ "timeEstimate": "45m",
+ "colour": "primary",
+ "required": true,
+ "filePath": "OSSM - PCB - 3030 Mount Cover.stl",
+ "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/PCB/OSSM%20-%20PCB%20-%203030%20Mount%20Cover.stl?raw=true"
+ }
+ ],
+ "hardwareParts": [
+ {
+ "id": "hardware-fasteners-m6x12-shcs",
+ "required": true,
+ "quantity": 4,
+ "relatedParts": [
+ "ossm-pcb-3030-mount"
+ ]
+ },
+ {
+ "id": "hardware-fasteners-m6-t-nuts",
+ "required": true,
+ "quantity": 4,
+ "relatedParts": [
+ "ossm-pcb-3030-mount"
+ ]
+ }
+ ]
+ },
+ "aio-cover-mount": {
+ "category": "PCB Mount",
+ "type": "base",
+ "printedParts": [
+ {
+ "id": "ossm-pcb-aio-cover-mount",
+ "name": "PCB AIO Cover Mount",
+ "description": "All-in-one cover mount on the actuator",
+ "filamentEstimate": 20,
+ "timeEstimate": "1h",
+ "colour": "primary",
+ "required": true,
+ "filePath": "OSSM - PCB - AIO Cover Mount.stl",
+ "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/PCB/OSSM%20-%20PCB%20-%20AIO%20Cover%20Mount.stl?raw=true"
+ }
+ ],
+ "hardwareParts": [
+ {
+ "id": "hardware-fasteners-m3x8-shcs",
+ "required": true,
+ "quantity": 4,
+ "relatedParts": [
+ "ossm-pcb-aio-cover-mount"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/website/src/data/components/stand.json b/website/src/data/components/stand.json
index 81b669e..9f6388c 100644
--- a/website/src/data/components/stand.json
+++ b/website/src/data/components/stand.json
@@ -18,7 +18,8 @@
"colour": "primary",
"required": true,
"filePath": "OSSM - Stand - Pivot Plate.stl",
- "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Left.stl?raw=true"
+ "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Left.stl?raw=true",
+ "quantity": 1
},
{
"id": "pivot-plate-right",
@@ -28,7 +29,19 @@
"colour": "primary",
"required": true,
"filePath": "OSSM - Stand - Pivot Plate.stl",
- "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Right.stl?raw=true"
+ "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Pivot%20Plate%20Right.stl?raw=true",
+ "quantity": 1
+ },
+ {
+ "id": "handle-spacer",
+ "name": "Handle Spacer",
+ "description": "Handle spacer for the stand",
+ "filamentEstimate": 20,
+ "colour": "primary",
+ "required": true,
+ "filePath": "OSSM - Stand - Pivot Spacer.stl",
+ "url": "https://github.com/KinkyMakers/OSSM-hardware/blob/main/Printed%20Parts/Stand/OSSM%20-%20Stand%20-%203030%20Extrusion%20Base%20-%20Handle%20Spacer.stl?raw=true",
+ "quantity": 8
}
],
"hardwareParts": [
@@ -67,7 +80,8 @@
"description": "Reinforced 3030 hinges for PitClamp",
"filamentEstimate": 200,
"colour": "primary",
- "required": true
+ "required": true,
+ "quantity": 1
}
],
"hardwareParts": [
diff --git a/website/src/data/index.js b/website/src/data/index.js
index 0eceef3..9231d14 100644
--- a/website/src/data/index.js
+++ b/website/src/data/index.js
@@ -8,6 +8,7 @@ import standComponents from './components/stand.json';
import mountingComponents from './components/mounting.json';
import toyMountsComponents from './components/toyMounts.json';
import remoteComponents from './components/remote.json';
+import pcbComponents from './components/pcb.json';
// Create a hardware lookup map from hardware.json
const hardwareLookup = new Map();
@@ -92,6 +93,7 @@ const rawComponents = {
...mountingComponents,
...toyMountsComponents,
...remoteComponents,
+ ...pcbComponents,
};
// Resolve hardware references
diff --git a/website/src/main.jsx b/website/src/main.jsx
index b9a1a6d..0aec8f2 100644
--- a/website/src/main.jsx
+++ b/website/src/main.jsx
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
+import { ThemeProvider } from './contexts/ThemeContext'
createRoot(document.getElementById('root')).render(
-
+
+
+
,
)
diff --git a/website/tailwind.config.js b/website/tailwind.config.js
index dca8ba0..9121d4e 100644
--- a/website/tailwind.config.js
+++ b/website/tailwind.config.js
@@ -4,6 +4,7 @@ export default {
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
+ darkMode: 'class',
theme: {
extend: {},
},