Edit in JSFiddle

const myPlugin = (editor) => {
  const category1 = { id: 'first', label: 'First category' };
  const category2 = { id: 'second', label: 'Second category' };
	
  // Demo component with some traits
  editor.Components.addType('demo-cmp', {
    model: {
      defaults: {
        traits: [
          {
            type: 'date',
            name: 'date-trait',
            label: 'Date trait',
            placeholder: 'Insert date',
          },
          {
            type: 'text',
            name: 'text-trait',
            label: false,
            placeholder: 'Insert text',
            category: category1,
          },
          {
            type: 'select',
            name: 'select-trait',
            label: 'Select 1',
            category: category1,
            default: 'opt1',
            options: [
              { id: 'opt1', name: 'Option 1'},
              { id: 'opt2', name: 'Option 2'},
              { id: 'opt3', name: 'Option 3'},
            ]
          },
          {
            type: 'select',
            name: 'select-trait2',
            label: 'Select 2',
            category: category1,
            default: 'opt2',
            options: [
              { id: 'opt1', name: 'Option 1'},
              { id: 'opt2', name: 'Option 2'},
              { id: 'opt3', name: 'Option 3'},
            ]
          },
          {
            type: 'number',
            name: 'number-trait',
            placeholder: '0-100',
            min: 0,
            max: 100,
            step: 5,
          },
          {
            type: 'color',
            label: 'Color trait',
            name: 'color-trait',
          },
          {
            type: 'checkbox',
            label: 'Checkbox trait',
            name: 'checkbox-trait',
            valueTrue: 'YES',
            valueFalse: 'NO',
          },
          {
            type: 'checkbox',
            name: 'open',
            category: category2,
          },
          {
            type: 'button',
            label: 'Button trait',
            labelButton: 'Alert',
            name: 'button-trait',
            category: category2,
            full: true,
            command: () => alert('hello'),
          },
          {
            type: 'button',
            label: false,
            full: true,
            category: category2,
            labelButton: 'Open code',
            name: 'button-trait2',
            command: 'core:open-code',
          },
        ],
      },
    },
  });
};
  
const editor = grapesjs.init({
  container: '#gjs',
  height: '100%',
  storageManager: false,
  noticeOnUnload: false,
  fromElement: true,
  plugins: ['gjs-blocks-basic', myPlugin],
  traitManager: {
    custom: true
  }
});

Vue.component('trait-field', {
  props: { trait: Object },
  template: '#trait-field',
  computed: {
    inputValue() {
      return this.trait.getValue({ useType: true });
    },
    type() {
      return this.trait.getType();
    },
    placeholder() {
      return this.trait.get('placeholder');
    },
    toOptions() {
      const { trait } = this;
      return trait.getOptions().map(o => ({ value: trait.getOptionId(o), text: trait.getOptionLabel(o) }))
    },
    isTypeNeedLabel() {
      return !['checkbox', 'button'].includes(this.type);
    }
  },
  methods: {
    handleChange(value) {
      this.trait.setValue(value);
    },
    handleInput(value) {
      this.trait.setValue(value, { partial: true });
    },
  }
});

const app = new Vue({
  el: '.vue-app',
  vuetify: new Vuetify({ theme: { dark: true } }),
  data: {
    traitCategories: [],
    panels: [],
  },
  mounted() {
    editor.on('trait:custom', this.onTraitCustom);
  },
  destroyed() {
    editor.off('trait:custom', this.onTraitCustom);
  },
  methods: {
    onTraitCustom(props) {
      const { container } = props;
      if (container && !container.contains(this.$el)) {
        container.appendChild(this.$el);
      }
      const traitsByCategory = editor.Traits.getTraitsByCategory();
      const noCategoryIndex = traitsByCategory.findIndex(trc => !trc.category) || 0;
      // Keep items without categories open
      if (!this.panels.includes(noCategoryIndex)) {
        this.panels.push(noCategoryIndex);
      }
      this.traitCategories = traitsByCategory;
    },
    categoryId(traitCategory) {
      return traitCategory.category?.id || 'none';
    },
  }
});
<div id="gjs">
  <div style="padding: 25px" data-gjs-type="demo-cmp">
    Custom Trait Manager (select me and check my traits)
  </div>
</div>

<div style="display: none;">
    <!-- Vue app  -->
  <div class="vue-app">
    <v-app>
      <v-main>
        <v-expansion-panels accordion multiple v-model="panels">
          <v-expansion-panel v-for="trc in traitCategories">
            <v-expansion-panel-header v-if="trc.category">
              {{ trc.category.getLabel() }}
            </v-expansion-panel-header>
            <v-expansion-panel-header class="no-cat-header" v-else></v-expansion-panel-header>
            <v-expansion-panel-content>
              <v-row>
                <trait-field v-for="trait in trc.items" :key="trait.id" :trait="trait"/>
              </v-row>
            </v-expansion-panel-content>
          </v-expansion-panel>
        </v-expansion-panels>
      </v-main>
    </v-app>
  </div>
  
  <!-- Trait Field template -->
  <div id="trait-field" style="display: none;">
    <v-col :class="['py-0 px-1 mb-1', trait.get('full') && 'mb-3']" :cols="12">
      <v-row class="flex-nowrap" v-if="isTypeNeedLabel">
        <v-col cols="auto pr-0">{{ trait.getLabel() }}</v-col>
      </v-row>
      <div v-if="type === 'number'">
        <v-text-field :placeholder="placeholder" :value="inputValue" @change="handleChange" outlined dense/>
      </div>
      <div v-else-if="type === 'checkbox'">
        <v-checkbox :label="trait.getLabel()" :input-value="inputValue" @change="handleChange"></v-checkbox>
      </div>
      <div v-else-if="type === 'select'">
        <v-select :items="toOptions" :value="inputValue" @change="handleChange" outlined dense/>
      </div>
      <div v-else-if="type === 'color'">
        <v-text-field :placeholder="placeholder" :value="inputValue" @change="handleChange" outlined dense>
          <template v-slot:append>
            <div :style="{ backgroundColor: inputValue || placeholder }" class="trait-color-prv">
              <input class="trait-input-color"
                      type="color"
                      :value="inputValue || placeholder"
                      @change="(ev) => handleChange(ev.target.value)"
                      @input="(ev) => handleInput(ev.target.value)"
                      />
            </div>
          </template>
        </v-text-field>
      </div>
      <div v-else-if="type === 'button'">
        <v-btn block @click="trait.runCommand()">
          <v-row>
            <v-col>{{ trait.get('labelButton') }}</v-col>
          </v-row>
        </v-btn>
      </div>
      <div v-else>
        <v-text-field :placeholder="placeholder" :type="type" :value="inputValue" @change="handleChange" outlined dense/>
      </div>
    </v-col>
  </div>
</div>
body, html {
  margin: 0;
  height: 100%;
}

.trait-input-color {
  width: 16px !important;
  height: 15px !important;
  opacity: 0 !important;
}

/* Vuetify overrides */
.v-application {
  background: transparent !important;
}
.v-application--wrap {
  min-height: auto;
}
.v-input__slot {
  font-size: 12px;
  min-height: 10px !important;
  color-scheme: dark;
}
.v-select__selections {
  flex-wrap: nowrap;
}
.v-text-field .v-input__slot {
  padding: 0 10px !important;
}
.v-input--selection-controls {
  margin-top: 0;
}
.v-text-field__details, .v-messages {
  display: none;
}
.no-cat-header {
  opacity: 0;
  padding: 10px;
  max-height: 10px;
  pointer-events: none;
  min-height: auto !important;
}
.trait-color-prv {
  border: 1px solid rgba(255,255,255,0.5);
  border-radius: 3px;
}