-
-
- Onshape Cutlist Generator
-
-
-
+
+
+
{{ doc.name }}
+
by {{ doc.owner.name }}
+
{{ url }}
- User Manual
-
-
•
-
- GitHub
-
-
-
-
-
-
- {{ doc.name }}
- by {{ doc.owner.name }}
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/web/components/PartListItem.vue b/web/components/PartListItem.vue
index 4754a61..4b5d1b1 100644
--- a/web/components/PartListItem.vue
+++ b/web/components/PartListItem.vue
@@ -37,7 +37,7 @@ const showPartNumbers = useShowPartNumbers();
>
{{ placement.partNumber }}
diff --git a/web/components/ProjectListItem.vue b/web/components/ProjectListItem.vue
new file mode 100644
index 0000000..5c34aa7
--- /dev/null
+++ b/web/components/ProjectListItem.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
{{ project.name }}
+
Onshape
+
+
+
+
+
diff --git a/web/components/StockMatrixInput.vue b/web/components/StockMatrixInput.vue
index eea8d53..3daf416 100644
--- a/web/components/StockMatrixInput.vue
+++ b/web/components/StockMatrixInput.vue
@@ -32,7 +32,7 @@ const error = ref();
+
+
+
+
diff --git a/web/components/TabListItem.vue b/web/components/TabListItem.vue
new file mode 100644
index 0000000..66362ec
--- /dev/null
+++ b/web/components/TabListItem.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
diff --git a/web/components/UPlaceholder.vue b/web/components/UPlaceholder.vue
index 41b42bc..3a84fc1 100644
--- a/web/components/UPlaceholder.vue
+++ b/web/components/UPlaceholder.vue
@@ -8,14 +8,16 @@ defineProps<{
@@ -24,7 +26,7 @@ defineProps<{
diff --git a/web/composables/deleteProject.ts b/web/composables/deleteProject.ts
new file mode 100644
index 0000000..ebeee04
--- /dev/null
+++ b/web/composables/deleteProject.ts
@@ -0,0 +1,9 @@
+export default function () {
+ const closeTab = useCloseTab();
+ const projects = useProjects();
+
+ return (id: string) => {
+ closeTab(id);
+ projects.value = projects.value.filter((project) => project.id !== id);
+ };
+}
diff --git a/web/composables/useCloseTab.ts b/web/composables/useCloseTab.ts
new file mode 100644
index 0000000..3601352
--- /dev/null
+++ b/web/composables/useCloseTab.ts
@@ -0,0 +1,10 @@
+export default function () {
+ const openIds = useOpenProjectIds();
+ const route = useRoute();
+ const router = useRouter();
+
+ return (id: string) => {
+ if (route.path === `/p/${id}`) router.replace('/');
+ openIds.value = openIds.value.filter((item) => item !== id);
+ };
+}
diff --git a/web/composables/useCreateNewProject.ts b/web/composables/useCreateNewProject.ts
new file mode 100644
index 0000000..2aeca02
--- /dev/null
+++ b/web/composables/useCreateNewProject.ts
@@ -0,0 +1,22 @@
+export default function () {
+ const { openDialog } = useDialogState();
+ const openProjectIds = useOpenProjectIds();
+ const allProjects = useProjects();
+ const router = useRouter();
+
+ const createNewProject = () =>
+ new Promise
((res, rej) => {
+ openDialog('add-project', {
+ title: 'New Project',
+ onAdd: res,
+ onCancel: () => rej(Error('Canceled')),
+ });
+ });
+
+ return async () => {
+ const project = await createNewProject();
+ allProjects.value.push(project);
+ openProjectIds.value.push(project.id);
+ router.push(`/p/${project.id}`);
+ };
+}
diff --git a/web/composables/useDeleteProject.ts b/web/composables/useDeleteProject.ts
new file mode 100644
index 0000000..0f38181
--- /dev/null
+++ b/web/composables/useDeleteProject.ts
@@ -0,0 +1,9 @@
+export default function () {
+ const projects = useProjects();
+ const closeTab = useCloseTab();
+
+ return (id: string) => {
+ closeTab(id);
+ projects.value = projects.value.filter((project) => project.id !== id);
+ };
+}
diff --git a/web/composables/useDialogState.ts b/web/composables/useDialogState.ts
new file mode 100644
index 0000000..dd03550
--- /dev/null
+++ b/web/composables/useDialogState.ts
@@ -0,0 +1,35 @@
+export default createGlobalState(() => {
+ const state = ref({});
+ const openDialog = (
+ name: T,
+ props: GetDialogProps,
+ ) => {
+ state.value = {
+ ...state.value,
+ [name]: props,
+ };
+ };
+ const closeDialog = (name: T) => {
+ state.value = {
+ ...state.value,
+ [name]: undefined,
+ };
+ };
+
+ return {
+ state,
+ openDialog,
+ closeDialog,
+ };
+});
+
+export interface DialogDefinition {
+ 'add-project': {
+ title: string;
+ onAdd: (project: Project) => void;
+ onCancel: () => void;
+ };
+}
+export type DialogName = keyof DialogDefinition;
+export type GetDialogProps = DialogDefinition[T];
+export type DialogState = Partial;
diff --git a/web/composables/useModalModel.ts b/web/composables/useModalModel.ts
new file mode 100644
index 0000000..ed3169b
--- /dev/null
+++ b/web/composables/useModalModel.ts
@@ -0,0 +1,14 @@
+import useDialogState, { type DialogName } from './useDialogState';
+
+export function useModalModel(name: DialogName) {
+ const { state, closeDialog } = useDialogState();
+
+ return computed({
+ get() {
+ return state.value[name] != null;
+ },
+ set(value) {
+ if (!value) closeDialog(name);
+ },
+ });
+}
diff --git a/web/composables/useOpenProject.ts b/web/composables/useOpenProject.ts
new file mode 100644
index 0000000..39dab52
--- /dev/null
+++ b/web/composables/useOpenProject.ts
@@ -0,0 +1,10 @@
+export default function () {
+ const openIds = useOpenProjectIds();
+ const router = useRouter();
+
+ return (id: string) => {
+ router.push(`/p/${id}`);
+
+ if (!openIds.value.includes(id)) openIds.value.push(id);
+ };
+}
diff --git a/web/composables/useOpenProjectIds.ts b/web/composables/useOpenProjectIds.ts
new file mode 100644
index 0000000..6cf2c6e
--- /dev/null
+++ b/web/composables/useOpenProjectIds.ts
@@ -0,0 +1,3 @@
+export default createGlobalState(() =>
+ useLocalStorage('@culist/open-project-ids', []),
+);
diff --git a/web/composables/useProjects.ts b/web/composables/useProjects.ts
new file mode 100644
index 0000000..63c02cb
--- /dev/null
+++ b/web/composables/useProjects.ts
@@ -0,0 +1,5 @@
+import type { Project } from '~/utils/projects';
+
+export default createGlobalState(() =>
+ useLocalStorage('@cutlist/projects', []),
+);
diff --git a/web/layouts/default.vue b/web/layouts/default.vue
new file mode 100644
index 0000000..03fb4a8
--- /dev/null
+++ b/web/layouts/default.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/pages/index.vue b/web/pages/index.vue
index d22103f..1a1477d 100644
--- a/web/pages/index.vue
+++ b/web/pages/index.vue
@@ -1,29 +1,37 @@
-
-
-
-
-
-
-
-
+
+
+
+
Projects
+ New
+
+
+
+
+ -
+ {{ projects.length }} projects
+
+
+
+
-
-
diff --git a/web/pages/p/[id].vue b/web/pages/p/[id].vue
new file mode 100644
index 0000000..1dde661
--- /dev/null
+++ b/web/pages/p/[id].vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/utils/projects.ts b/web/utils/projects.ts
new file mode 100644
index 0000000..3d17109
--- /dev/null
+++ b/web/utils/projects.ts
@@ -0,0 +1,8 @@
+export interface Project {
+ id: string;
+ name: string;
+ source: {
+ type: 'onshape';
+ url: string;
+ };
+}