diff --git css/export-ui-list.css css/export-ui-list.css
new file mode 100644
index 0000000..3a778ab
--- /dev/null
+++ css/export-ui-list.css
@@ -0,0 +1,28 @@
+/* $Id: panels-item.css,v 1.1.2.1 2010/02/17 01:09:46 merlinofchaos Exp $ */
+body form#ctools-export-ui-list-form {
+  margin: 0 0 20px 0;
+}
+
+#ctools-export-ui-list-form .form-item {
+  padding-right: 1em; /* LTR */
+  float: left; /* LTR */
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+#ctools-export-ui-list-items {
+  width: 100%;
+}
+
+#edit-order-wrapper {
+  clear: left; /* LTR */
+}
+
+#ctools-export-ui-list-form .form-submit {
+  margin-top: 1.65em;
+  float: left; /* LTR */
+}
+
+tr.ctools-export-ui-disabled {
+  color: #999;
+}
diff --git ctools.module ctools.module
index 4d120b3..e8ada61 100644
--- ctools.module
+++ ctools.module
@@ -284,7 +284,7 @@ function _ctools_passthrough(&$items, $type = 'theme') {
     require_once './' . $file->filename;
     list($tool) = explode('.', $file->name, 2);
 
-    $function = 'ctools_' . $tool . '_' . $type;
+    $function = 'ctools_' . str_replace ('-', '_', $tool) . '_' . $type;
     if (function_exists($function)) {
       $function($items);
     }
@@ -760,3 +760,46 @@ function ctools_file_check_directory(&$directory, $mode = 0, $form_item = NULL)
 
   return TRUE;
 }
+
+/**
+ * Menu loader; Load exportables when used with export-ui.
+ */
+function ctools_export_ui_load($item_name, $plugin_name) {
+  $return = &ctools_static(__FUNCTION__, FALSE);
+
+  if (!$return) {
+    ctools_include('export');
+    ctools_include('export-ui');
+    $plugin = ctools_get_export_ui($plugin_name);
+
+    if ($plugin) {
+      // Get the load callback.
+      $schema = ctools_export_get_schema($plugin['schema']);
+      $return = call_user_func($schema['export']['load callback'], $item_name);
+    }
+  }
+
+  return $return;
+}
+
+/**
+ * Menu access callback for various tasks of export-ui.
+ */
+function ctools_export_ui_task_access($export, $op) {
+  // TODO: This needs to be more configurable.
+
+  if (!user_access('administer site configuration')) {
+    return FALSE;
+  }
+  switch ($op) {
+    case 'revert':
+      return ($export->export_type & EXPORT_IN_DATABASE) && ($export->export_type & EXPORT_IN_CODE);
+    case 'delete':
+      return ($export->export_type & EXPORT_IN_DATABASE) && !($export->export_type & EXPORT_IN_CODE);
+    case 'disable':
+      return empty($export->disabled);
+    case 'enable':
+      return !empty($export->disabled);
+  }
+  return TRUE;
+}
diff --git includes/export-ui.admin.inc includes/export-ui.admin.inc
new file mode 100644
index 0000000..ff243d8
--- /dev/null
+++ includes/export-ui.admin.inc
@@ -0,0 +1,213 @@
+<?php
+// $Id:$
+
+/**
+ * Main page callback to list items.
+ *
+ * This simply loads the object defined in the plugin and hands it off.
+ */
+function ctools_export_ui_list_items($plugin_name) {
+  $js = !empty($_REQUEST['ctools_ajax']);
+
+  // Load the $plugin information
+  ctools_include('export-ui');
+  ctools_include('export');
+  $plugin = ctools_get_export_ui($plugin_name);
+
+  $handler = ctools_export_ui_get_handler($plugin);
+  if ($handler) {
+    return $handler->list_page($js, $_POST);
+  }
+  else {
+    return t('Configuration error. No handler found.');
+  }
+}
+
+/**
+ * Main page callback to manipulate exportables.
+ *
+ * This simply loads the object defined in the plugin and hands it off to
+ * a method based upon the name of the operation in use. This can easily
+ * be used to add more ops.
+ */
+function ctools_export_ui_switcher_page($plugin_name, $op) {
+  $args = func_get_args();
+  $js = !empty($_REQUEST['ctools_ajax']);
+
+  // Load the $plugin information
+  ctools_include('export-ui');
+  ctools_include('export');
+  $plugin = ctools_get_export_ui($plugin_name);
+
+  $handler = ctools_export_ui_get_handler($plugin);
+  if ($handler) {
+    $method = $op . '_page';
+    if (method_exists($handler, $method)) {
+      // replace the first two arguments:
+      $args[0] = $js;
+      $args[1] = $_POST;
+      return call_user_func_array(array($handler, $method), $args);
+    }
+  }
+  else {
+    return t('Configuration error. No handler found.');
+  }
+}
+
+/**
+ * Provide a form to confirm one of the provided actions.
+ */
+function ctools_export_ui_confirm(&$form_state, $plugin_name, $op = 'delete', $export) {
+  $plugin = ctools_get_export_ui($plugin_name);
+
+  $form = array();
+  $form['export'] = array('#type' => 'value', '#value' => $export);
+  $form['action'] = array('#type' => 'value', '#value' => $op);
+  $form['plugin'] = array('#type' => 'value', '#value' => $plugin);
+
+  $export_key = $plugin['export']['key'];
+  $question = str_replace('!action', $plugin['allowed operations'][$op], $plugin['form']['string']['confirmation']['question']);
+  $question = str_replace('%title', $export->{$export_key}, $plugin['form']['string']['confirmation']['question']);
+
+  $form = confirm_form($form,
+    $question,
+    _ctools_export_ui_redirect($plugin['name']),
+    $plugin['form']['string']['confirmation'][$op],
+    drupal_ucfirst($plugin['allowed operations'][$op]), t('Cancel')
+  );
+  return $form;
+}
+
+/**
+ * Submit handler for the ctools_export_ui_confirm form.
+ */
+function ctools_export_ui_confirm_submit($form, &$form_state) {
+  ctools_include('export');
+  $plugin = $form['plugin']['#value'];
+
+  $export = $form_state['values']['export'];
+  switch ($form_state['values']['action']) {
+    case 'revert':
+    case 'delete':
+      $schema = ctools_export_get_schema($plugin['schema']);
+      call_user_func($schema['export']['delete callback'], $export);
+      break;
+   }
+  $form_state['redirect'] = _ctools_export_ui_redirect($plugin['name']);
+}
+
+/**
+ * Enable or disable an exportable.
+ */
+function ctools_export_ui_switcher(&$form_state, $plugin_name, $op = 'enable', $export) {
+  $plugin = ctools_get_export_ui($plugin_name);
+  $export_key = $plugin['export']['key'];
+
+  ctools_export_set_object_status($export, $op != 'enable');
+  drupal_set_message(str_replace('%title', $export->{$export_key}, $plugin['form']['string']['message'][$op]));
+  drupal_goto(_ctools_export_ui_redirect($plugin['name']));
+}
+
+/**
+ * Page callback for import form. Switches form output to export form
+ * if import submission has occurred.
+ */
+function ctools_export_ui_import_page($plugin_name) {
+  if (!empty($_POST) && $_POST['form_id'] == 'ctools_export_ui_form') {
+    return drupal_get_form('ctools_export_ui_form', $plugin_name, 'add');
+  }
+  return drupal_get_form('ctools_export_ui_import', $plugin_name);
+}
+
+/**
+ * Import form. Provides simple helptext instructions and textarea for
+ * pasting a export definition.
+ */
+function ctools_export_ui_import($form_state, $plugin_name) {
+  ctools_include('export-ui');
+  $plugin = ctools_get_export_ui($plugin_name);
+
+  drupal_set_title($plugin['form']['string']['title']['import']);
+
+  $form = array();
+  $form['plugin'] = array(
+    '#type' => 'value',
+    '#value' => $plugin,
+  );
+
+  $form['help'] = array(
+    '#type' => 'item',
+    '#value' => $plugin['form']['string']['help']['import'],
+  );
+  $form['import'] = array(
+    '#title' => t('@plugin object', array('@plugin' => $plugin['title'])),
+    '#type' => 'textarea',
+    '#rows' => 10,
+    '#required' => TRUE,
+  );
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Import'),
+  );
+  return $form;
+}
+
+/**
+ * Import form submit handler. Evaluates import code and transfers to
+ * export definition form.
+ */
+function ctools_export_ui_import_submit($form, &$form_state) {
+  $plugin = $form['plugin']['#value'];
+  $export_key = $plugin['export']['key'];
+
+  $items = array();
+  if ($import = $form_state['values']['import']) {
+    ob_start();
+    $export = eval($import);
+    ob_end_clean();
+  }
+
+  if (is_object($export)) {
+    if (!empty($export->{$export_key})) {
+
+      $schema = ctools_export_get_schema($plugin['schema']);
+
+      // TODO: If $schema['export']['load callback'] == FALSE we should issue
+      // an error - but maybe it should happen in a more generic place.
+      if (call_user_func($schema['export']['load callback'], $export->{$export_key})) {
+        drupal_set_message(t('A @plugin with this name already exists. Please remove the existing export before importing this definition.', array('@plugin' => $plugin['title'])), 'error');
+      }
+      else {
+        drupal_set_title($plugin['form']['string']['title']['export']);
+        $output = drupal_get_form('ctools_export_ui_form', $plugin['name'], 'add', (object) $export);
+        print theme('page', $output);
+        exit;
+      }
+    }
+  }
+  else {
+    drupal_set_message(t('An error occurred while importing. Please check your export definition.', 'error'));
+
+    $form_state['redirect'] = _ctools_export_ui_redirect($plugin['name']);
+  }
+}
+
+/**
+ * Provides a form with an exported export definition for use in modules.
+ *
+ * @param $cid
+ *   A export id.
+ *
+ * @return
+ *   A FormAPI array.
+ */
+function ctools_export_ui_export(&$form_state, $export, $plugin_name) {
+  ctools_include('export-ui');
+  $plugin = ctools_get_export_ui($plugin_name);
+
+  $export_key = $plugin['export']['key'];
+
+  drupal_set_title(t('Export %title', array('%title' => $export->{$export_key})));
+
+  return ctools_export_form($form_state, ctools_export_ui_export_object($plugin, $export), t('Export'));
+}
diff --git includes/export-ui.inc includes/export-ui.inc
new file mode 100644
index 0000000..0f9d3cb
--- /dev/null
+++ includes/export-ui.inc
@@ -0,0 +1,352 @@
+<?php
+// $Id: export_ui.module,v 1.13.2.48.2.1.2.11 2010/02/28 19:30:48 yhahn Exp $
+
+/**
+ * Implementation of hook_ctools_plugin_*.
+ *
+ * Give information to CTools about the content types plugin.
+ */
+function ctools_ctools_plugin_export_ui() {
+  return array(
+    'defaults' => 'ctools_export_ui_defaults',
+  );
+}
+
+/**
+ * Provide defaults for an export-ui plugin.
+ */
+function ctools_export_ui_defaults($info, &$plugin) {
+  ctools_include('export');
+
+  $plugin += array(
+    'has menu' => TRUE,
+    'title' => $plugin['name'],
+    'export' => array(),
+    'allowed operations' => array(),
+    'menu' => array(),
+    'form' => array(),
+    'list' => NULL,
+  );
+
+  if (empty($plugin['schema'])) {
+    if ($plugin['has menu']) {
+      // We need to issue a warning as schema is a required key.
+      drupal_set_message(t('The plugin definition of @plugin is missing the "schema" key.', array('@plugin' => $plugin['name'])), 'error');
+    }
+  }
+  else {
+    $schema = ctools_export_get_schema($plugin['schema']);
+
+    $plugin['export'] += array(
+      // Add the identifier key from the schema so we don't have to call
+      // ctools_export_get_schema() just for that.
+      'key' => $schema['export']['key'],
+    );
+
+    // Add some default fields that appear often in exports
+    // If these use different keys they can easily be specified in the
+    // $plugin.
+
+    if (empty($plugin['export']['admin_title']) && !empty($schema['fields']['admin_title'])) {
+      $plugin['export']['admin_title'] = 'admin_title';
+    }
+    if (empty($plugin['export']['admin_description']) && !empty($schema['fields']['admin_description'])) {
+      $plugin['export']['admin_description'] = 'admin_description';
+    }
+  }
+
+  // Define allowed operations, and the name of the operations.
+  $plugin['allowed operations'] += array(
+    'edit'    => t('Edit'),
+    'enable'  => t('Enable'),
+    'disable' => t('Disable'),
+    'revert'  => t('Revert'),
+    'delete'  => t('Delete'),
+    'clone'   => t('Clone'),
+    'import'  => t('Import'),
+    'export'  => t('Export'),
+  );
+
+  if ($plugin['has menu']) {
+    $plugin['menu'] += array(
+      'menu item' => str_replace(' ', '-', $plugin['name']),
+      'menu prefix' => 'admin/build',
+    );
+
+    $prefix_count = count(explode('/', $plugin['menu']['menu prefix']));
+
+    $plugin['menu'] += array(
+      // Default menu items that should be declared.
+      'items' => array(
+        'list callback' => array(
+          'path' => '',
+          // Menu items are translated by the menu system.
+          // TODO: We need more flexibility in title. The title of the admin page
+          // is not necessarily the title of the object, plus we need
+          // plural, singular, proper, not proper, etc.
+          'title' => $plugin['title'],
+          'description' => 'List '. $plugin['title'],
+          'page callback' => 'ctools_export_ui_list_items',
+          'page arguments' => array($plugin['name']),
+          'type' => MENU_NORMAL_ITEM,
+        ),
+        'list' => array(
+          'path' => 'list',
+          'title' => 'List',
+          'description' => 'List '. $plugin['title'],
+          'page callback' => 'ctools_export_ui_list_items',
+          'page arguments' => array( $plugin['name']),
+          'type' => MENU_DEFAULT_LOCAL_TASK,
+        ),
+        'add' => array(
+          'path' => 'add',
+          'title' => 'Add',
+          'description' => 'Add a new '. $plugin['title'],
+          'page callback' => 'ctools_export_ui_switcher_page',
+          'page arguments' => array($plugin['name'], 'add'),
+          'type' => MENU_LOCAL_TASK,
+        ),
+        'edit callback' => array(
+          'path' => 'list/%ctools_export_ui',
+          'page callback' => 'ctools_export_ui_switcher_page',
+          'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'type' => MENU_CALLBACK,
+        ),
+        'edit' => array(
+          'path' => 'list/%ctools_export_ui/edit',
+          'title' => 'Edit',
+          'page callback' => 'ctools_export_ui_switcher_page',
+          'page arguments' => array($plugin['name'], 'edit', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'type' => MENU_DEFAULT_LOCAL_TASK,
+        ),
+      ),
+    );
+
+    if ($plugin['allowed operations']['import']) {
+      $plugin['menu']['items'] += array(
+        'import' => array(
+          'path' => 'import',
+          'title' => 'Import',
+          'description' => 'Import '. $plugin['title'] .' to your site.',
+          // We allow permissions to import only to users that are allowed to
+          // execute php.
+          'access callback' => 'ctools_access_multiperm',
+          'access arguments' => array('use PHP for block visibility'),
+          'page callback' => 'ctools_export_ui_import_page',
+          'page arguments' => array($plugin['name']),
+          'type' => MENU_LOCAL_TASK,
+        ),
+      );
+    }
+
+    if ($plugin['allowed operations']['export']) {
+      $plugin['menu']['items'] += array(
+        'export' => array(
+          'path' => 'list/%ctools_export_ui/export',
+          'title' => 'Export',
+          'description' => 'Export '. $plugin['title'],
+          'page callback' => 'drupal_get_form',
+          'page arguments' => array('ctools_export_ui_export', $prefix_count + 2, $plugin['name']),
+          'load arguments' => array($plugin['name']),
+          'type' => MENU_LOCAL_TASK,
+        ),
+      );
+    }
+
+    if ($plugin['allowed operations']['revert']) {
+      $plugin['menu']['items'] += array(
+        'revert' => array(
+          'path' => 'list/%ctools_export_ui/revert',
+          'title' => 'Revert',
+          'description' => 'Revert '. $plugin['title'],
+          'page callback' => 'drupal_get_form',
+          'page arguments' => array('ctools_export_ui_confirm', $plugin['name'], 'revert', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'access callback' => 'ctools_export_ui_task_access',
+          'access arguments' => array($prefix_count + 2, 'revert'),
+          'type' => MENU_CALLBACK,
+        ),
+      );
+    }
+
+    if ($plugin['allowed operations']['clone']) {
+      $plugin['menu']['items'] += array(
+        'clone' => array(
+          'path' => 'list/%ctools_export_ui/clone',
+          'title' => 'Clone',
+          'description' => 'Revert '. $plugin['title'],
+          'page callback' => 'ctools_export_ui_switcher_page',
+          'page arguments' => array($plugin['name'], 'clone', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'type' => MENU_CALLBACK,
+        ),
+      );
+    }
+
+    if ($plugin['allowed operations']['delete']) {
+      $plugin['menu']['items'] += array(
+        'delete' => array(
+          'path' => 'list/%ctools_export_ui/delete',
+          'title' => 'Delete',
+          'description' => 'Delete '. $plugin['title'],
+          'page callback' => 'drupal_get_form',
+          'page arguments' => array('ctools_export_ui_confirm', $plugin['name'], 'delete', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'access callback' => 'ctools_export_ui_task_access',
+          'access arguments' => array($prefix_count + 2, 'delete'),
+          'type' => MENU_CALLBACK,
+        ),
+      );
+    }
+
+    if ($plugin['allowed operations']['enable']) {
+      $plugin['menu']['items'] += array(
+        'enable' => array(
+          'path' => 'list/%ctools_export_ui/enable',
+          'title' => 'Enable',
+          'description' => 'Enable '. $plugin['title'],
+          'page callback' => 'ctools_export_ui_switcher_page',
+          'page arguments' => array($plugin['name'], 'enable', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'access callback' => 'ctools_export_ui_task_access',
+          'access arguments' => array($prefix_count + 2, 'enable'),
+          'type' => MENU_CALLBACK,
+        ),
+      );
+    }
+
+    if ($plugin['allowed operations']['disable']) {
+      $plugin['menu']['items'] += array(
+        'disable' => array(
+          'path' => 'list/%ctools_export_ui/disable',
+          'title' => 'Disable',
+          'description' => 'Disable '. $plugin['title'],
+          'page callback' => 'ctools_export_ui_switcher_page',
+          'page arguments' => array($plugin['name'], 'disable', $prefix_count + 2),
+          'load arguments' => array($plugin['name']),
+          'access callback' => 'ctools_export_ui_task_access',
+          'access arguments' => array($prefix_count + 2, 'disable'),
+          'type' => MENU_CALLBACK,
+        ),
+      );
+    }
+  }
+
+  // Define form elements.
+  $plugin['form'] += array(
+      'settings' => $plugin['name'] . '_form',
+      'string' => array(
+        // Strings used in drupal_set_title().
+        'title' => array(
+          'add' => t('Add a new @plugin', array('@plugin' => $plugin['title'])),
+          // The "%title" will be replaced in ctools_export_ui_form(), as in this
+          // stage we dont have the specific exportable object.
+          'edit' => t('Editing @plugin %title', array('@plugin' => $plugin['title'])),
+          'view' => t('Viewing @plugin %title', array('@plugin' => $plugin['title'])),
+          'clone' => t('Cloning @plugin %title', array('@plugin' => $plugin['title'])),
+
+          'import' => t('Import @plugin', array('@plugin' => $plugin['title'])),
+          'export' => t('Export @plugin', array('@plugin' => $plugin['title'])),
+        ),
+        // Strings used in confirmation pages.
+        'confirmation' => array(
+           // The "!action" and "%title" will be replaced in
+           // ctools_export_ui_form().
+          'question' => t('Are you sure you want to !action the @plugin %title?', array('@plugin' => $plugin['title'])),
+          'revert' => t('This action will permanently remove any customizations made to this export.'),
+          'delete' => t('This action will remove this export permanently from your site.'),
+        ),
+        // Strings used in $forms.
+        'help' => array(
+          'import' => t('You can import an exported definition by pasting the exported object code into the field below.'),
+        ),
+        // Strings used in drupal_set_message().
+        'message' => array(
+          'enable' => t('@plugin %title was enabled.', array('@plugin' => $plugin['title'])),
+          'disable' => t('@plugin %title was disabled.', array('@plugin' => $plugin['title'])),
+        ),
+      ),
+  );
+}
+
+/**
+ * Get the class to handle creating a list of exportable items.
+ *
+ * @return
+ *   Either the lister class or FALSE if one could not be had.
+ */
+function ctools_export_ui_get_handler($plugin) {
+  $cache = &ctools_static(__FUNCTION__, array());
+  if (empty($cache[$plugin['name']])) {
+    // If a list class is not specified by the plugin, fall back to the
+    // default ctools_export_ui plugin instead.
+    if (empty($plugin['list'])) {
+      $default = ctools_get_export_ui('ctools_export_ui');
+      $class = ctools_plugin_get_class($default, 'handler');
+    }
+    else {
+      $class = ctools_plugin_get_class($plugin, 'handler');
+    }
+
+    if ($class) {
+      $cache[$plugin['name']] = new $class();
+      $cache[$plugin['name']]->init($plugin);
+    }
+  }
+  return !empty($cache[$plugin['name']]) ? $cache[$plugin['name']] : FALSE;
+}
+
+/**
+ * CTools export function.
+ */
+function ctools_export_ui_export_object($plugin_name, $export, $indent = '') {
+  $schema = ctools_export_get_schema($plugin['schema']);
+
+  // TODO: Add translatable fields.
+
+  // TODO: The below doesn't actually work for everything, as the schema isn't
+  // always the identifier. Plus, standard exports don't include this, so
+  // why are we adding it?
+//  $extra ="\nreturn \$". $plugin['schema'] .";\n";
+
+  ctools_include('export');
+  return ctools_export_object($plugin['schema'], $export, $indent);
+}
+
+/**
+ * Get redirection path from a plugin.
+ *
+ * @param $plguin_name
+ *   The plugin name.
+ *
+ * @return
+ *   The menu path to the plugin's list.
+ */
+function _ctools_export_ui_redirect($plugin_name) {
+  ctools_include('plugins');
+  $plugin = ctools_get_plugins('ctools', 'export_ui', $plugin_name);
+
+  return $plugin['menu']['menu prefix'] .'/'. $plugin['menu']['menu item'];
+}
+
+/**
+ * Helper function to include CTools plugins and get an export-ui exportable.
+ *
+ * @param $plugin_name
+ *   The plugin that should be laoded.
+ */
+function ctools_get_export_ui($plugin_name) {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'export_ui', $plugin_name);
+
+}
+
+/**
+ * Helper function to include CTools plugins and get all export-ui exportables.
+ */
+function ctools_get_export_uis() {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'export_ui', $plugin_name);
+}
diff --git includes/export-ui.menu.inc includes/export-ui.menu.inc
new file mode 100644
index 0000000..cdea750
--- /dev/null
+++ includes/export-ui.menu.inc
@@ -0,0 +1,33 @@
+<?php
+// $Id: export_ui.module,v 1.13.2.48.2.1.2.11 2010/02/28 19:30:48 yhahn Exp $
+
+
+/**
+ * Delegated implementation of hook_menu().
+ */
+function ctools_export_ui_menu(&$items) {
+  ctools_include('plugins');
+  // TODO: Maybe the include should be done in ctools_get_plugins()?
+  ctools_include('export-ui');
+
+  foreach (ctools_get_plugins('ctools', 'export_ui') as $plugin) {
+    if ($plugin['has menu']) {
+      $prefix = $plugin['menu']['menu prefix'] .'/'. $plugin['menu']['menu item'];
+
+      foreach ($plugin['menu']['items'] as $item) {
+        // Add menu item defaults.
+        $item += array(
+          'access arguments' => array('administer site configuration'),
+          'file' => 'export-ui.admin.inc',
+          'file path' => drupal_get_path('module', 'ctools') .'/includes',
+          // Add the map, so we can get the plugin in export_ui_load().
+          'load arguments' => array('%map'),
+        );
+
+        $path = !empty($item['path']) ? $prefix .'/'. $item['path'] : $prefix;
+        unset($item['path']);
+        $items[$path] = $item;
+      }
+    }
+  }
+}
diff --git includes/export.inc includes/export.inc
index b465d38..f3d2277 100644
--- includes/export.inc
+++ includes/export.inc
@@ -460,6 +460,13 @@ function ctools_export_get_schema($table) {
     'export callback' => "$schema[module]_export_{$table}",
     'list callback' => "$schema[module]_{$table}_list",
     'to hook code callback' => "$schema[module]_{$table}_to_hook_code",
+
+    // Define CRUD functions, if none are defined in the exportable
+    // schema.
+    'create callback' => 'ctools_export_new_object',
+    'save callback'   => function_exists("$schema[module]_save") ? "$schema[module]_save" : FALSE,
+    'delete callback' => function_exists("$schema[module]_delete") ? "$schema[module]_delete" : FALSE,
+    'load callback'   => function_exists("$schema[module]_load") ? "$schema[module]_load" : FALSE,
   );
 
   return $schema;
diff --git js/auto-submit.js js/auto-submit.js
new file mode 100644
index 0000000..fa35bfb
--- /dev/null
+++ js/auto-submit.js
@@ -0,0 +1,66 @@
+// $Id: auto-submit.js,v 1.1.2.1 2010/02/17 01:09:46 merlinofchaos Exp $
+
+/**
+ * To make a form auto submit, all you have to do is 3 things:
+ *
+ * ctools_add_js('auto-submit');
+ *
+ * On gadgets you want to auto-submit when changed, add the ctools-auto-submit
+ * class. With FAPI, add:
+ * @code
+ *  '#attributes' => array('class' => 'ctools-auto-submit'),
+ * @endcode
+ *
+ * Finally, you have to identify which button you want clicked for autosubmit.
+ * The behavior of this button will be honored if it's ajaxy or not:
+ * @code
+ *  '#attributes' => array('class' => 'ctools-use-ajax ctools-auto-submit-click'),
+ * @endcode
+ *
+ * Currently only 'select' and 'textfield' types are supported. We probably
+ * could use additional support for radios and checkboxes.
+ */
+
+Drupal.behaviors.CToolsAutoSubmit = function() {
+  var timeoutID = 0;
+
+  // Bind to any select widgets that will be auto submitted.
+  $('select.ctools-auto-submit:not(.ctools-auto-submit-processed)')
+    .addClass('.ctools-auto-submit-processed')
+    .change(function() {
+      $(this.form).find('.ctools-auto-submit-click').click();
+    });
+
+  // Bind to any textfield widgets that will be auto submitted.
+  $('input[type=text].ctools-auto-submit:not(.ctools-auto-submit-processed)')
+    .addClass('.ctools-auto-submit-processed')
+    .keyup(function(e) {
+      var form = this.form;
+      switch (e.keyCode) {
+        case 16: // shift
+        case 17: // ctrl
+        case 18: // alt
+        case 20: // caps lock
+        case 33: // page up
+        case 34: // page down
+        case 35: // end
+        case 36: // home
+        case 37: // left arrow
+        case 38: // up arrow
+        case 39: // right arrow
+        case 40: // down arrow
+        case 9:  // tab
+        case 13: // enter
+        case 27: // esc
+          return false;
+        default:
+          if (!$(form).hasClass('ctools-ajaxing')) {
+            if ((timeoutID)) {
+              clearTimeout(timeoutID);
+            }
+
+            timeoutID = setTimeout(function() { $(form).find('.ctools-auto-submit-click').click(); }, 300);
+        }
+      }
+    });
+}
diff --git plugins/export_ui/ctools_export_ui.class.php plugins/export_ui/ctools_export_ui.class.php
new file mode 100644
index 0000000..ba85a4e
--- /dev/null
+++ plugins/export_ui/ctools_export_ui.class.php
@@ -0,0 +1,849 @@
+<?php
+// $Id:$
+
+/**
+ * Base class for export UI.
+ */
+class ctools_export_ui {
+  var $plugin;
+  var $name;
+  var $options = array();
+
+  /**
+   * Fake constructor -- this is easier to deal with than the real
+   * constructor because we are retaining PHP4 compatibility, which
+   * would require all child classes to implement their own constructor.
+   */
+  function init($plugin) {
+    ctools_include('export');
+
+    $this->plugin = $plugin;
+  }
+
+  // ------------------------------------------------------------------------
+  // These methods are the API for generating the list of exportable items.
+
+  /**
+   * Master entry point for handling a list.
+   *
+   * It is unlikely that a child object will need to override this method,
+   * unless the listing mechanism is going to be highly specialized.
+   */
+  function list_page($js, $input) {
+    ctools_export_load_object_reset($this->plugin['schema']);
+    // TODO: Probably should be a callback in the schema to handle this in
+    // case the exports want a more complicated load function.
+    $this->items = ctools_export_load_object($this->plugin['schema'], 'all');
+
+    // Respond to a reset command by clearing session and doing a drupal goto
+    // back to the base URL.
+    if (isset($input['op']) && $input['op'] == t('Reset')) {
+      unset($_SESSION['ctools_export_ui'][$this->plugin['name']]);
+      if (!$js) {
+        return drupal_goto($_GET['q']);
+      }
+      // clear everything but form id, form build id and form token:
+      $keys = array_keys($input);
+      foreach ($keys as $id) {
+        if ($id != 'form_id' && $id != 'form_build_id' && $id != 'form_token') {
+          unset($input[$id]);
+        }
+      }
+      $replace_form = TRUE;
+    }
+
+    // If there is no input, check to see if we have stored input in the
+    // session.
+    if (!isset($input['form_id'])) {
+      if (isset($_SESSION['ctools_export_ui'][$this->plugin['name']]) && is_array($_SESSION['ctools_export_ui'][$this->plugin['name']])) {
+        $input  = $_SESSION['ctools_export_ui'][$this->plugin['name']];
+      }
+    }
+    else {
+      $_SESSION['ctools_export_ui'][$this->plugin['name']] = $input;
+      unset($_SESSION['ctools_export_ui'][$this->plugin['name']]['q']);
+    }
+
+    // This is where the form will put the output.
+    $this->rows = array();
+    $this->sorts = array();
+
+    $form_state = array(
+      'plugin' => $this->plugin,
+      'input' => $input,
+      'rerender' => TRUE,
+      'no_redirect' => TRUE,
+      'object' => &$this,
+    );
+
+    ctools_include('form');
+    $form = ctools_build_form('ctools_export_ui_list_form', $form_state);
+
+    $output = $this->list_header($form_state) . $this->list_render($form_state) . $this->list_footer($form_state);
+
+    if (!$js) {
+      $this->list_css();
+      return $form . $output;
+    }
+
+    ctools_include('ajax');
+    $commands = array();
+    $commands[] = ctools_ajax_command_replace('#ctools-export-ui-list-items', $output);
+    if (!empty($replace_form)) {
+      $commands[] = ctools_ajax_command_replace('#ctools-export-ui-list-form', $form);
+    }
+    ctools_ajax_render($commands);
+  }
+
+  /**
+   * Create the filter/sort form at the top of a list of exports.
+   *
+   * This handles the very default conditions, and most lists are expected
+   * to override this and call through to parent::list_form() in order to
+   * get the base form and then modify it as necessary to add search
+   * gadgets for custom fields.
+   */
+  function list_form(&$form, &$form_state) {
+    // This forces the form to *always* treat as submitted which is
+    // necessary to make it work.
+    $form['#token'] = FALSE;
+    if (empty($form_state['input'])) {
+      $form["#programmed"] = TRUE;
+    }
+
+    // Add the 'q' in if we are not using clean URLs or it can get lost when
+    // using this kind of form.
+    if (!variable_get('clean_url', FALSE)) {
+      $form['q'] = array(
+        '#type' => 'hidden',
+        '#value' => $_GET['q'],
+      );
+    }
+
+    $all = array('all' => t('- All -'));
+
+    $form['top row'] = array(
+      '#prefix' => '<div class="ctools-export-ui-row ctools-export-ui-top-row clear-block">',
+      '#suffix' => '</div>',
+    );
+
+    $form['bottom row'] = array(
+      '#prefix' => '<div class="ctools-export-ui-row ctools-export-ui-bottom-row clear-block">',
+      '#suffix' => '</div>',
+    );
+
+    $form['top row']['storage'] = array(
+      '#type' => 'select',
+      '#title' => t('Storage'),
+      '#options' => $all + array(
+        t('Normal') => t('Normal'),
+        t('Default') => t('Default'),
+        t('Overridden') => t('Overridden'),
+      ),
+      '#default_value' => 'all',
+      '#attributes' => array('class' => 'ctools-auto-submit'),
+    );
+
+    $form['top row']['disabled'] = array(
+      '#type' => 'select',
+      '#title' => t('Enabled'),
+      '#options' => $all + array(
+        '0' => t('Enabled'),
+        '1' => t('Disabled')
+      ),
+      '#default_value' => 'all',
+      '#attributes' => array('class' => 'ctools-auto-submit'),
+    );
+
+    $form['top row']['search'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Search'),
+      '#attributes' => array('class' => 'ctools-auto-submit'),
+    );
+
+    $form['bottom row']['order'] = array(
+      '#type' => 'select',
+      '#title' => t('Sort by'),
+      '#options' => array(
+        $this->list_sort_options(),
+      ),
+      '#default_value' => 'disabled',
+      '#attributes' => array('class' => 'ctools-auto-submit'),
+    );
+
+    $form['bottom row']['sort'] = array(
+      '#type' => 'select',
+      '#title' => t('Order'),
+      '#options' => array(
+        'asc' => t('Up'),
+        'desc' => t('Down'),
+      ),
+      '#default_value' => 'asc',
+      '#attributes' => array('class' => 'ctools-auto-submit'),
+    );
+
+    $form['bottom row']['submit'] = array(
+      '#type' => 'submit',
+      '#id' => 'ctools-export-ui-list-items-apply',
+      '#value' => t('Apply'),
+      '#attributes' => array('class' => 'ctools-use-ajax ctools-auto-submit-click'),
+    );
+
+    $form['bottom row']['reset'] = array(
+      '#type' => 'submit',
+      '#id' => 'ctools-export-ui-list-items-apply',
+      '#value' => t('Reset'),
+      '#attributes' => array('class' => 'ctools-use-ajax'),
+    );
+
+    ctools_add_js('ajax-responder');
+    ctools_add_js('auto-submit');
+    drupal_add_js('misc/jquery.form.js');
+    ctools_add_js('export-ui-list.js');
+
+    $form['#prefix'] = '<div class="clear-block">';
+    $form['#suffix'] = '</div>';
+
+
+  }
+
+  /**
+   * Validate the filter/sort form.
+   *
+   * It is very rare that a filter form needs validation, but if it is
+   * needed, override this.
+   */
+  function list_form_validate(&$form, &$form_state) { }
+
+  /**
+   * Submit the filter/sort form.
+   *
+   * This submit handler is actually responsible for building up all of the
+   * rows that will later be rendered, since it is doing the filtering and
+   * sorting.
+   *
+   * For the most part, you should not need to override this method, as the
+   * fiddly bits call through to other functions.
+   */
+  function list_form_submit(&$form, &$form_state) {
+    // Filter and re-sort the pages.
+    $plugin = $this->plugin;
+
+    $prefix = $plugin['menu']['menu prefix'] . '/' . $plugin['menu']['menu item'];
+
+    foreach ($this->items as $name => $item) {
+      // Call through to the filter and see if we're going to render this
+      // row. If it returns TRUE, then this row is filtered out.
+      if ($this->list_filter($form_state, $item)) {
+        continue;
+      }
+
+      $operations = array();
+
+      // TODO: We want to return this to just a foreach loop but we need
+      // some additional data.
+      if ($plugin['allowed operations']['edit']) {
+        $operations[] = array(
+          'title' => $plugin['allowed operations']['edit'],
+          'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['edit']['path']),
+        );
+      }
+
+      if (empty($item->disabled) && $plugin['allowed operations']['disable']) {
+        $operations[] = array(
+          'title' => $plugin['allowed operations']['disable'],
+          'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['disable']['path']),
+          'attributes' => array('class' => 'ctools-use-ajax'),
+        );
+      }
+
+      if (!empty($item->disabled) && $plugin['allowed operations']['enable']) {
+        $operations[] = array(
+          'title' => $plugin['allowed operations']['enable'],
+          'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['enable']['path']),
+          'attributes' => array('class' => 'ctools-use-ajax'),
+        );
+      }
+
+      switch ($item->type) {
+        case t('Normal'):
+          if ($plugin['allowed operations']['delete']) {
+            $operations[] = array(
+              'title' => $plugin['allowed operations']['delete'],
+              'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['delete']['path']),
+            );
+          }
+          break;
+        case t('Overridden'):
+          if ($plugin['allowed operations']['revert']) {
+            $operations[] = array(
+              'title' => $plugin['allowed operations']['revert'],
+              'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['revert']['path']),
+            );
+          }
+          break;
+      }
+
+      if ($plugin['allowed operations']['export']) {
+        $operations[] = array(
+          'title' => $plugin['allowed operations']['export'],
+          'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['export']['path']),
+        );
+      }
+
+      if ($plugin['allowed operations']['clone']) {
+        $operations[] = array(
+          'title' => $plugin['allowed operations']['clone'],
+          'href' => $prefix . '/' . str_replace('%ctools_export_ui', $name, $plugin['menu']['items']['clone']['path']),
+        );
+      }
+
+      $this->list_build_row($item, $form_state, $operations);
+    }
+
+    // Now actually sort
+    if ($form_state['values']['sort'] == 'desc') {
+      arsort($this->sorts);
+    }
+    else {
+      asort($this->sorts);
+    }
+
+    // Nuke the original.
+    $rows = $this->rows;
+    $this->rows = array();
+    // And restore.
+    foreach ($this->sorts as $name => $title) {
+      $this->rows[$name] = $rows[$name];
+    }
+  }
+
+  /**
+   * Determine if a row should be filtered out.
+   *
+   * This handles the default filters for the export UI list form. If you
+   * added additional filters in list_form() then this is where you should
+   * handle them.
+   *
+   * @return
+   *   TRUE if the item should be excluded.
+   */
+  function list_filter($form_state, $item) {
+    if ($form_state['values']['storage'] != 'all' && $form_state['values']['storage'] != $item->type) {
+      return TRUE;
+    }
+
+    if ($form_state['values']['disabled'] != 'all' && $form_state['values']['disabled'] != !empty($item->disabled)) {
+      return TRUE;
+    }
+
+    if ($form_state['values']['search']) {
+      $search = strtolower($form_state['values']['search']);
+      foreach ($this->list_search_fields() as $field) {
+        if (strpos(strtolower($item->$field), $search) !== FALSE) {
+          $hit = TRUE;
+          break;
+        }
+      }
+      if (empty($hit)) {
+        return TRUE;
+      }
+    }
+  }
+
+  /**
+   * Provide a list of fields to test against for the default "search" widget.
+   *
+   * This widget will search against whatever fields are configured here. By
+   * default it will attempt to search against the name, title and description fields.
+   */
+  function list_search_fields() {
+    $fields = array(
+      $this->plugin['export']['key'],
+    );
+
+    if (!empty($this->plugin['export']['admin_title'])) {
+      $fields[] = $this->plugin['export']['admin_title'];
+    }
+    if (!empty($this->plugin['export']['admin_description'])) {
+      $fields[] = $this->plugin['export']['admin_description'];
+    }
+
+    return $fields;
+  }
+
+  /**
+   * Provide a list of sort options.
+   *
+   * Override this if you wish to provide more or change how these work.
+   * The actual handling of the sorting will happen in build_row().
+   */
+  function list_sort_options() {
+    if (!empty($this->plugin['export']['admin_title'])) {
+      $options = array(
+        'disabled' => t('Enabled, title'),
+        $this->plugin['export']['admin_title'] => t('Title'),
+      );
+    }
+    else {
+      $options = array(
+        'disabled' => t('Enabled, name'),
+      );
+    }
+
+    $options += array(
+      'name' => t('Name'),
+      'storage' => t('Storage'),
+    );
+
+    return $options;
+  }
+
+  /**
+   * Add listing CSS to the page.
+   *
+   * Override this if you need custom CSS for your list.
+   */
+  function list_css() {
+    ctools_add_css('export-ui-list');
+  }
+
+  /**
+   * Build a row based on the item.
+   *
+   * By default all of the rows are placed into a table by the render
+   * method, so this is building up a row suitable for theme('table').
+   * This doesn't have to be true if you override both.
+   */
+  function list_build_row($item, &$form_state, $operations) {
+    // Set up sorting
+    $name = $item->{$this->plugin['export']['key']};
+
+    // Note: $item->type should have already been set up by export.inc so
+    // we can use it safely.
+    switch ($form_state['values']['order']) {
+      case 'disabled':
+        $this->sorts[$name] = empty($item->disabled) . $name;
+        break;
+      case 'title':
+        $this->sorts[$name] = $item->{$this->plugin['export']['admin_title']};
+        break;
+      case 'name':
+        $this->sorts[$name] = $name;
+        break;
+      case 'storage':
+        $this->sorts[$name] = $item->type . $name;
+        break;
+    }
+
+    $class =
+
+    $this->rows[$name]['data'] = array();
+    $this->rows[$name]['class'] = !empty($item->disabled) ? 'ctools-export-ui-disabled' : 'ctools-export-ui-enabled';
+
+    // If we have an admin title, make it the first row.
+    if ($this->plugin['export']['admin_title']) {
+      $this->rows[$name]['data'][] = array('data' => check_plain($item->{$this->plugin['export']['admin_title']}), 'class' => 'ctools-export-ui-title');
+    }
+    $this->rows[$name]['data'][] = array('data' => check_plain($name), 'class' => 'ctools-export-ui-name');
+    $this->rows[$name]['data'][] = array('data' => check_plain($item->type), 'class' => 'ctools-export-ui-storage');
+    $this->rows[$name]['data'][] = array('data' => theme('links', $operations), 'class' => 'ctools-export-ui-operations');
+
+    // Add an automatic mouseover of the description if one exists.
+    if (!empty($this->plugin['export']['admin_description'])) {
+      $this->rows[$name]['title'] = $item->{$this->plugin['export']['admin_description']};
+    }
+  }
+
+  /**
+   * Provide the table header.
+   *
+   * If you've added columns via list_build_row() but are still using a
+   * table, override this method to set up the table header.
+   */
+  function list_table_header() {
+    $header = array();
+    if ($this->plugin['export']['admin_title']) {
+      $header[] = array('data' => t('Title'), 'class' => 'ctools-export-ui-title');
+    }
+
+    $header[] = array('data' => t('Name'), 'class' => 'ctools-export-ui-name');
+    $header[] = array('data' => t('Storage'), 'class' => 'ctools-export-ui-storage');
+    $header[] = array('data' => t('Operations'), 'class' => 'ctools-export-ui-operations');
+
+    return $header;
+  }
+
+  /**
+   * Render all of the rows together.
+   *
+   * By default we place all of the rows in a table, and this should be the
+   * way most lists will go.
+   *
+   * Whatever you do if this method is overridden, the ID is important for AJAX
+   * so be sure it exists.
+   */
+  function list_render(&$form_state) {
+    return theme('table', $this->list_table_header(), $this->rows, array('id' => 'ctools-export-ui-list-items'));
+  }
+
+  /**
+   * Render a header to go before the list.
+   *
+   * This will appear after the filter/sort widgets.
+   */
+  function list_header($form_state) { }
+
+  /**
+   * Render a footer to go after thie list.
+   *
+   * This is a good place to add additional links.
+   */
+  function list_footer($form_state) { }
+
+  // ------------------------------------------------------------------------
+  // These methods are the API for adding/editing exportable items
+
+  function add_page($js, $input) {
+    $form_state = array(
+      'plugin' => $this->plugin,
+      'object' => &$this,
+      'ajax' => $js,
+      'item' => ctools_export_new_object($this->plugin['schema']),
+      'op' => 'add',
+      'rerender' => TRUE,
+      'no_redirect' => TRUE,
+    );
+
+    return $this->edit_execute_form($form_state);
+  }
+
+  /**
+   * Main entry point to edit an item.
+   *
+   * The default implementation simply uses a form, so this should be
+   * overridden for more complex implentations that need more than to display
+   * a simple form (like a view or a page manager page).
+   */
+  function edit_page($js, $input, $item) {
+    $form_state = array(
+      'plugin' => $this->plugin,
+      'object' => &$this,
+      'ajax' => $js,
+      'item' => $item,
+      'op' => 'edit',
+      'rerender' => TRUE,
+      'no_redirect' => TRUE,
+    );
+
+    return $this->edit_execute_form($form_state);
+  }
+
+  function clone_page($js, $input, $item) {
+    // To make a clone of an item, we first export it and then re-import it.
+    // Export the handler, which is a fantastic way to clean database IDs out of it.
+    $schema = ctools_export_get_schema($this->plugin['schema']);
+    ctools_include('export');
+    $export = ctools_export_object($this->plugin['schema'], $item);
+
+    ob_start();
+    eval($export);
+    ob_end_clean();
+
+    if (empty(${$schema['export']['identifier']})) {
+      return drupal_not_found();
+    }
+
+    $item = ${$schema['export']['identifier']};
+    $item->{$this->plugin['export']['key']} = 'clone_of_' . $item->name;
+
+    // Set these defaults just the same way that ctools_export_new_object sets them.
+    $item->export_type = EXPORT_IN_DATABASE;
+    $item->type = t('Local');
+
+    $form_state = array(
+      'plugin' => $this->plugin,
+      'object' => &$this,
+      'ajax' => $js,
+      'item' => $item,
+      'op' => 'add',
+      'rerender' => TRUE,
+      'no_redirect' => TRUE,
+    );
+
+    return $this->edit_execute_form($form_state);
+  }
+
+  /**
+   * Execute the form.
+   *
+   * Add and Edit both funnel into this, but they have a few different
+   * settings.
+   */
+  function edit_execute_form($form_state) {
+    ctools_include('form');
+    $output = ctools_build_form('ctools_export_ui_edit_item_form', $form_state);
+    if (!empty($form_state['executed'])) {
+      $item = &$form_state['item'];
+      $export_key = $this->plugin['export']['key'];
+
+      $schema = ctools_export_get_schema($this->plugin['schema']);
+      $result = $schema['export']['save callback']($item);
+
+      if ($result) {
+        drupal_set_message(t('Saved @plugin %title.', array('@plugin' => $this->plugin['title'], '%title' =>  $item->{$export_key})));
+      }
+      else {
+        drupal_set_message(t('Could not save @plugin %title.', array('@plugin' => $this->plugin['title'], '%title' =>  $item->{$export_key})), 'error');
+      }
+    }
+
+    return $output;
+  }
+
+  /**
+   * Provide the actual editing form.
+   */
+  function edit_form(&$form, &$form_state) {
+    $export_key = $this->plugin['export']['key'];
+    $item = $form_state['item'];
+
+    // Set title.
+    if ($form_state['op'] == 'edit') {
+      if ($item->export_type & EXPORT_IN_DATABASE) {
+        $title_op = $form_state['op'];
+      }
+      else {
+        $title_op = 'view';
+      }
+    }
+    else {
+      $title_op = $form_state['op'];
+    }
+
+    // Replace %title that might be there with the exportable title.
+    drupal_set_title(str_replace('%title', $item->{$export_key}, $this->plugin['form']['string']['title'][$title_op]));
+
+    // TODO: Drupal 7 has a nifty method of auto guessing names from
+    // titles that is standard. We should integrate that here as a
+    // nice standard.
+    // Guess at a couple of our standard fields.
+    if (!empty($this->plugin['export']['admin_title'])) {
+      $form['info'][$this->plugin['export']['admin_title']] = array(
+        '#type' => 'textfield',
+        '#title' => t('Administrative title'),
+        '#description' => t('This will appear in the administrative interface to easily identify it.'),
+        '#default_value' => $item->{$this->plugin['export']['admin_title']},
+      );
+    }
+
+    $form['info'][$export_key] = array(
+      // TODO: Add human readable name on key in export.inc?
+      '#title' => ucfirst($export_key),
+      '#type' => 'textfield',
+      '#default_value' => $item->{$export_key},
+      '#description' => t('The unique ID for this @export', array('@export' => $this->plugin['title'])),
+      '#required' => TRUE,
+      '#maxlength' => 255,
+    );
+
+    if ($form_state['op'] === 'edit') {
+      $form['info'][$export_key]['#disabled'] = TRUE;
+      $form['info'][$export_key]['#value'] = $item->{$export_key};
+    }
+    else {
+      $form['info'][$export_key]['#element_validate'] = array('ctools_export_ui_edit_name_validate');
+    }
+
+    /**
+     * -- tag isn't currently a standard field on exportables, so this probably
+     * should not be here.
+
+    $form['info']['tag'] = array(
+      '#title' => t('Tag'),
+      '#type' => 'textfield',
+      '#default_value' => !empty($export->tag) ? $export->tag : '',
+      '#description' => t('Tag for this @export', array('@export' => $title)),
+    );
+     */
+
+    if (!empty($this->plugin['export']['admin_description'])) {
+      $form['info'][$this->plugin['export']['admin_description']] = array(
+        '#type' => 'textarea',
+        '#title' => t('Administrative description'),
+        '#default_value' => $item->{$this->plugin['export']['admin_description']},
+      );
+    }
+
+    // Make sure that whatever happens, the buttons go to the bottom.
+    $form['buttons']['#weight'] = 100;
+
+    // Add buttons.
+    $form['buttons']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+    );
+
+    $form['buttons']['delete'] = array(
+      '#type' => 'submit',
+      '#value' => $item->export_type & EXPORT_IN_CODE ? t('Revert') : t('Delete'),
+      '#access' => $form_state['op'] === 'edit' && $item->export_type & EXPORT_IN_DATABASE,
+      '#submit' => 'ctools_export_ui_edit_name_validate',
+    );
+
+  }
+
+  function edit_form_validate(&$form, &$form_state) { }
+
+  /**
+   * Handle the submission of the edit form.
+   *
+   * At this point, submission is successful. Our only responsibility is
+   * to copy anything out of values onto the item that we are able to edit.
+   *
+   * If the keys all match up to the schema, this method will not need to be
+   * overridden.
+   */
+  function edit_form_submit(&$form, &$form_state) {
+    $plugin = $form_state['plugin'];
+    $export_key = $plugin['export']['key'];
+    $item = $form_state['item'];
+
+    $schema = ctools_export_get_schema($plugin['schema']);
+    foreach (array_keys($schema['fields']) as $key) {
+      if(isset($form_state['values'][$key])) {
+        $item->{$key} = $form_state['values'][$key];
+      }
+    }
+  }
+
+  // ------------------------------------------------------------------------
+  // These methods are the API for 'other' stuff with exportables such as
+  // enable, disable, import, export
+
+  /**
+   * Callback to enable a page.
+   */
+  function enable_page($js, $input, $item) {
+    return $this->set_item_state(FALSE, $js, $input, $item);
+  }
+
+  /**
+   * Callback to disable a page.
+   */
+  function disable_page($js, $input, $item) {
+    return $this->set_item_state(TRUE, $js, $input, $item);
+  }
+
+  /**
+   * Set an item's state to enabled or disabled and output to user.
+   *
+   * If javascript is in use, this will rebuild the list and send that back
+   * as though the filter form had been executed.
+   */
+  function set_item_state($state, $js, $input, $item) {
+    ctools_export_set_object_status($item, $state);
+
+    if (!$js) {
+      drupal_goto(_ctools_export_ui_redirect($this->plugin['name']));
+    }
+    else {
+      return $this->list_page($js, $input);
+    }
+  }
+
+  function export_page() {
+
+  }
+
+  function import_page() {
+
+  }
+
+}
+
+// -----------------------------------------------------------------------
+// Forms to be used with this class.
+//
+// Since Drupal's forms are completely procedural, these forms will
+// mostly just be pass-throughs back to the object.
+
+/**
+ * Form callback to handle the filter/sort form when listing items.
+ *
+ * This simply loads the object defined in the plugin and hands it off.
+ */
+function ctools_export_ui_list_form(&$form_state) {
+  $form = array();
+  $form_state['object']->list_form($form, $form_state);
+  return $form;
+}
+
+/**
+ * Validate handler for ctools_export_ui_list_form.
+ */
+function ctools_export_ui_list_form_validate(&$form, &$form_state) {
+  $form_state['object']->list_form_validate($form, $form_state);
+}
+
+/**
+ * Submit handler for ctools_export_ui_list_form.
+ */
+function ctools_export_ui_list_form_submit(&$form, &$form_state) {
+  $form_state['object']->list_form_submit($form, $form_state);
+}
+
+/**
+ * Form callback to edit an exportable item.
+ *
+ * This simply loads the object defined in the plugin and hands it off.
+ */
+function ctools_export_ui_edit_item_form(&$form_state) {
+  $form = array();
+  $form_state['object']->edit_form($form, $form_state);
+  return $form;
+}
+
+/**
+ * Validate handler for ctools_export_ui_edit_item_form.
+ */
+function ctools_export_ui_edit_item_form_validate(&$form, &$form_state) {
+  $form_state['object']->edit_form_validate($form, $form_state);
+}
+
+/**
+ * Submit handler for ctools_export_ui_edit_item_form.
+ */
+function ctools_export_ui_edit_item_form_submit(&$form, &$form_state) {
+  $form_state['object']->edit_form_submit($form, $form_state);
+}
+
+/**
+ * Submit handler to delete for ctools_export_ui_edit_item_form
+ */
+function ctools_export_ui_edit_item_form_delete(&$form, &$form_state) {
+  $plugin = $form_state['plugin'];
+  $export_key = $plugin['export']['key'];
+  $item = $form_state['item'];
+
+  $menu_prefix = _ctools_export_ui_redirect($plugin['name']);
+  $form_state['redirect'] = "$menu_prefix/list/$item->{$export_key}/delete";
+}
+
+/**
+ * Validate that an export item name is acceptable and unique during add.
+ */
+function ctools_export_ui_edit_name_validate($element, &$form_state) {
+  $plugin = $form_state['plugin'];
+  // Check for string identifier sanity
+  if (!preg_match('!^[a-z0-9_]+$!', $element['#value'])) {
+    form_error($element, t('The export id can only consist of lowercase letters, underscores, and numbers.'));
+    return;
+  }
+  // Check for name collision
+  $schema = ctools_export_get_schema($plugin['schema']);
+
+  if ($exists = call_user_func($schema['export']['load callback'], $element['#value'])) {
+    form_error($element, t('A @plugin with this name already exists. Please choose another name or delete the existing export before creating a new one.', array('@plugin' => $plugin['title'])));
+  }
+}
diff --git plugins/export_ui/ctools_export_ui.inc plugins/export_ui/ctools_export_ui.inc
new file mode 100644
index 0000000..1f93710
--- /dev/null
+++ plugins/export_ui/ctools_export_ui.inc
@@ -0,0 +1,19 @@
+<?php
+// $Id:$
+
+/**
+ * TODO: Document plugin keys:
+ * - name: The name of the plugin. This should be the same as the file name.
+ * - has menu: Deterimne if hook_menu() should declare items of this plugin.
+ *   Defaults to TRUE?
+ * - menu item: Optional; The menu item that should be used. For example if the
+ *   value is set to 'foo', then the URL will admin/build/foo. If no value is
+ *   defined then the plugin name will be used.
+ */
+$plugin = array(
+  'handler' => array(
+    'class' => 'ctools_export_ui',
+  ),
+  // As this is the base class plugin, it shouldn't declare any menu items.
+  'has menu' => FALSE,
+);
