diff --git a/package-lock.json b/package-lock.json
index cdf54218f7..76aac68ad8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -98,6 +98,7 @@
"vite-plugin-electron": "0.29.0",
"vite-tsconfig-paths": "5.1.2",
"vitest": "2.1.2",
+ "vue-component-type-helpers": "2.1.6",
"vue-tsc": "2.1.10",
"yargs": "17.2.1"
},
@@ -16205,13 +16206,20 @@
}
}
},
- "node_modules/vue-component-type-helpers": {
+ "node_modules/vue-component-meta/node_modules/vue-component-type-helpers": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.10.tgz",
"integrity": "sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==",
"dev": true,
"license": "MIT"
},
+ "node_modules/vue-component-type-helpers": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz",
+ "integrity": "sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vue-docgen-api": {
"version": "4.79.2",
"resolved": "https://registry.npmjs.org/vue-docgen-api/-/vue-docgen-api-4.79.2.tgz",
diff --git a/package.json b/package.json
index a7f9ba22d9..114b642d4b 100644
--- a/package.json
+++ b/package.json
@@ -135,6 +135,7 @@
"vite-plugin-electron": "0.29.0",
"vite-tsconfig-paths": "5.1.2",
"vitest": "2.1.2",
+ "vue-component-type-helpers": "2.1.6",
"vue-tsc": "2.1.10",
"yargs": "17.2.1"
}
diff --git a/src/components/Menu/ContextMenu/Presentation.vue b/src/components/Menu/ContextMenu/Presentation.vue
index da26802ac7..29ad0d6346 100644
--- a/src/components/Menu/ContextMenu/Presentation.vue
+++ b/src/components/Menu/ContextMenu/Presentation.vue
@@ -38,6 +38,9 @@ defineProps<{
uiLocked?: boolean;
}>();
defineExpose({
+ show: (event?: MouseEvent | undefined) => {
+ contextMenu.value?.show(event);
+ },
hide: () => {
contextMenu.value?.hide();
},
diff --git a/src/components/Sing/ChangeValueDialog/CommonDialog.vue b/src/components/Sing/ChangeValueDialog/CommonDialog.vue
new file mode 100644
index 0000000000..2e619dbcf8
--- /dev/null
+++ b/src/components/Sing/ChangeValueDialog/CommonDialog.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+
+ {{ props.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts
new file mode 100644
index 0000000000..1027e0e929
--- /dev/null
+++ b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts
@@ -0,0 +1,101 @@
+import { userEvent, within, expect, fn } from "@storybook/test";
+import { Meta, StoryObj } from "@storybook/vue3";
+
+import TempoChangeDialog from "./TempoChangeDialog.vue";
+
+const meta: Meta = {
+ component: TempoChangeDialog,
+ args: {
+ modelValue: true,
+ tempoChange: undefined,
+ mode: "add",
+
+ onOk: fn(),
+ onHide: fn(),
+ },
+ tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const CreateOpened: Story = {
+ name: "開いている:追加",
+ args: {
+ modelValue: true,
+ mode: "add",
+ },
+};
+export const ChangeOpened: Story = {
+ name: "開いている:変更",
+ args: {
+ modelValue: true,
+ tempoChange: {
+ bpm: 120,
+ },
+ mode: "edit",
+ },
+};
+
+export const ClickOk: Story = {
+ name: "OKボタンを押す:追加",
+ args: { ...CreateOpened.args },
+ play: async ({ args }) => {
+ const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う
+
+ const input = canvas.getByLabelText("テンポ");
+ await userEvent.clear(input);
+ await userEvent.type(input, "100");
+
+ const button = canvas.getByRole("button", { name: /追加する/ });
+ await userEvent.click(button);
+
+ await expect(args["onOk"]).toBeCalledWith({
+ tempoChange: {
+ bpm: 100,
+ },
+ });
+ },
+};
+
+export const ClickDelete: Story = {
+ name: "OKボタンを押す:編集",
+ args: { ...ChangeOpened.args },
+ play: async ({ args }) => {
+ const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う
+
+ const input = canvas.getByLabelText("テンポ");
+ await userEvent.clear(input);
+ await userEvent.type(input, "100");
+
+ const button = canvas.getByRole("button", { name: /変更する/ });
+ await userEvent.click(button);
+
+ await expect(args["onOk"]).toBeCalledWith({
+ tempoChange: {
+ bpm: 100,
+ },
+ });
+ },
+};
+
+export const CancelClose: Story = {
+ name: "キャンセルボタンを押す",
+ args: { ...ChangeOpened.args },
+ play: async ({ args }) => {
+ const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う
+
+ const button = canvas.getByRole("button", { name: /キャンセル/ });
+ await userEvent.click(button);
+
+ await expect(args["onOk"]).not.toBeCalled();
+ },
+};
+
+export const Closed: Story = {
+ name: "閉じている",
+ tags: ["skip-screenshot"],
+ args: {
+ modelValue: false,
+ },
+};
diff --git a/src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue
new file mode 100644
index 0000000000..6330c72ae8
--- /dev/null
+++ b/src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue
@@ -0,0 +1,49 @@
+
+ $emit('ok', { tempoChange })"
+ @hide="() => $emit('hide')"
+ >
+
+
+
+
+
+
+
diff --git a/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.stories.ts b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.stories.ts
new file mode 100644
index 0000000000..56be9ca2de
--- /dev/null
+++ b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.stories.ts
@@ -0,0 +1,104 @@
+import { userEvent, within, expect, fn } from "@storybook/test";
+import { Meta, StoryObj } from "@storybook/vue3";
+
+import TimeSignatureChangeDialog from "./TimeSignatureChangeDialog.vue";
+
+const meta: Meta = {
+ component: TimeSignatureChangeDialog,
+ args: {
+ modelValue: true,
+ timeSignatureChange: undefined,
+ mode: "add",
+
+ onOk: fn(),
+ onHide: fn(),
+ },
+ tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const CreateOpened: Story = {
+ name: "開いている:追加",
+ args: {
+ modelValue: true,
+ mode: "add",
+ },
+};
+export const ChangeOpened: Story = {
+ name: "開いている:変更",
+ args: {
+ modelValue: true,
+ timeSignatureChange: {
+ beats: 4,
+ beatType: 4,
+ },
+ mode: "edit",
+ },
+};
+
+export const ClickOk: Story = {
+ name: "OKボタンを押す:追加",
+ args: { ...CreateOpened.args },
+ play: async ({ args }) => {
+ const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う
+
+ const input = canvas.getByLabelText("拍子の分子");
+ await userEvent.clear(input);
+ await userEvent.type(input, "3");
+
+ const button = canvas.getByRole("button", { name: /追加する/ });
+ await userEvent.click(button);
+
+ await expect(args["onOk"]).toBeCalledWith({
+ timeSignatureChange: {
+ beats: 3,
+ beatType: 4,
+ },
+ });
+ },
+};
+
+export const ClickDelete: Story = {
+ name: "OKボタンを押す:編集",
+ args: { ...ChangeOpened.args },
+ play: async ({ args }) => {
+ const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う
+
+ const input = canvas.getByLabelText("拍子の分子");
+ await userEvent.clear(input);
+ await userEvent.type(input, "6");
+
+ const button = canvas.getByRole("button", { name: /変更する/ });
+ await userEvent.click(button);
+
+ await expect(args["onOk"]).toBeCalledWith({
+ timeSignatureChange: {
+ beats: 6,
+ beatType: 4,
+ },
+ });
+ },
+};
+
+export const CancelClose: Story = {
+ name: "キャンセルボタンを押す",
+ args: { ...ChangeOpened.args },
+ play: async ({ args }) => {
+ const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う
+
+ const button = canvas.getByRole("button", { name: /キャンセル/ });
+ await userEvent.click(button);
+
+ await expect(args["onOk"]).not.toBeCalled();
+ },
+};
+
+export const Closed: Story = {
+ name: "閉じている",
+ tags: ["skip-screenshot"],
+ args: {
+ modelValue: false,
+ },
+};
diff --git a/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.vue b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.vue
new file mode 100644
index 0000000000..a9e44e7a34
--- /dev/null
+++ b/src/components/Sing/ChangeValueDialog/TimeSignatureChangeDialog.vue
@@ -0,0 +1,72 @@
+
+ $emit('ok', { timeSignatureChange })"
+ @hide="() => $emit('hide')"
+ >
+
+ /
+
+
+
+
+
+
+
diff --git a/src/components/Sing/SequencerGrid/Presentation.vue b/src/components/Sing/SequencerGrid/Presentation.vue
index 8b6f9fb5c5..ac19ace72d 100644
--- a/src/components/Sing/SequencerGrid/Presentation.vue
+++ b/src/components/Sing/SequencerGrid/Presentation.vue
@@ -8,9 +8,10 @@
>
@@ -19,49 +20,67 @@
:key="`cell-${index}`"
x="0"
:y="gridCellHeight * index"
- :width="beatWidth * beatsPerMeasure"
- :height="gridCellHeight"
+ :width="gridCellWidth"
+ :height="gridCellHeight * 12"
:class="`sequencer-grid-cell sequencer-grid-cell-${keyInfo.color}`"
/>
+
+
-
+
+
+