Survey Slate | Designer Tools
Create, edit, and connect questions for both simple and complex surveys using a variety of input types. Also check out the User Guide for Survey Slate Designer.
test credentials for demoEditor (note - these credentials were blocked after being exposed on GitHug, so they don't fully work for testing)
~~~js
{
"accessKeyId": "AKIAQO7DBPIFDAUBK4SL",
"secretAccessKey": "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"
}
~~~
const login = applyCredentials(
manualCredentials
)
display(manualCredentials)
Designer UI
questionsNoLayout
///!!!!!!!!!!!!!!!!!!!!!!!!!
///!!! Check if this should self-execute
///!!!!!!!!!!!!!!!!!!!!!!!!!
const syncSurveyUiInputToSurveyUi = () => {
console.log("syncSurveyUiInputToSurveyUi");
//if (!_.isEqual(viewof surveyUi.value, viewof surveyUiInput.value)) {
//!!! Note: Chaning due to different definition of viewof
//if (!_.isEqual(surveyUi.value, surveyUiInput.value)) {
if (!_.isEqual(surveyUiElement.value, surveyUiInputElement.value)) {
console.log("syncSurveyUiInputToSurveyUi: change detected");
//!!! Note: Chaning due to different definition of viewof
//viewof surveyUi.value = viewof surveyUiInput.value;
//surveyUi.value = surveyUiInput.value;
surveyUiElement.value = surveyUiInput.value;
// Manually updating the UI state
// viewof surveyUi.applyValueUpdates();
//viewof surveyUi.dispatchEvent(new Event('input', {bubbles: true}))
//!!! Note: Chaning due to different definition of viewof
//surveyUi.dispatchEvent(new Event('input', {bubbles: true}))
surveyUiElement.dispatchEvent(new Event('input', {bubbles: true}))
}
}
///!!!!!!!!!!!!!!!!!!!!!!!!!
///!!! Check if this should self-execute
///!!!!!!!!!!!!!!!!!!!!!!!!!
const syncSurveyOutput = () => {
console.log("surveyOutput")
// convert ui representation (pages -> cells) to {questions, layout, config} for storage.
if (surveyUiOutput.pages.length === 0) return invalidation;
// Extract questions
const questions = new Map();
surveyUiOutput.pages.forEach(page => {
page.cells.forEach(cell => {
questions.set(cell.id, uiCellToQuestion({
...cell.inner.result,
type: cell.inner.type,
}))
})
});
// Extract layout
const layout = [];
surveyUiOutput.pages.forEach(page => {
page.cells.forEach(cell => {
const connections = cell?.connections?.connections || []
const set = connections.map(c => c.set).join(",");
layout.push({
id: cell.id,
menu: page.title,
set,
role: set === "" ? "" : connections.map(c => c.role).join(","),
})
})
});
// Extract config
/// NOTE: Adjusted to account for different definition of viewof under Framework
const config = {
// ...viewof surveyConfig.value, // carry over initial state
// ...surveyConfig.value, // carry over initial state
...surveyConfig, // carry over initial state
pageTitle: surveyUiOutput.metadata.title
};
// viewof surveyOutput.value = {
//surveyOutput.value = {
surveyOutputElement.value = {
questions,
layout,
config
};
//viewof surveyOutput.dispatchEvent(new Event('input', {bubbles: true}))
//surveyOutput.dispatchEvent(new Event('input', {bubbles: true}))
surveyOutputElement.dispatchEvent(new Event('input', {bubbles: true}))
};
display(syncSurveyOutput)
Autosave UI
Export
Persistence
SyntaxError: Unexpected token (7:9)
SyntaxError: Unexpected token (7:9)
const revertChanges = async function() {
//const version = await files.load(viewof settings.value.versions.at(-1))
const version = await files.load(settings.value.versions.at(-1))
//viewof settings.value = {
settings.value = {
//...viewof settings.value,
...settings.value,
configs: [
//...(viewof settings.value.configs || []),
...(settings.value.configs || []),
version.config
],
questions: [
//...(viewof settings.value.questions || []),
...(settings.value.questions || []),
version.questions
],
layout: [
//...(viewof settings.value.layout || []),
...(settings.value.layout || []),
version.layout
]
};
//await files.save("settings.json", viewof settings.value);
await files.save("settings.json", settings.value);
//viewof survey.dispatchEvent(new Event('input', {bubbles: true})) // reload everything
survey.dispatchEvent(new Event('input', {bubbles: true})) // reload everything
}
//!!!!!!!!!!!!!!!!!
// NOTE: re-defining to work with Framework's Mutable
//!!!!!!!!!!!!!!!!!
//const initialQuestionLoader = {
// if (!initialLoadQuestions) {
// mutable initialLoadQuestions = true;
// viewof questions.value = await loadQuestions(settings.questions[settings.questions.length - 1])
// viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
// }
// return "Initial Question Loader"
//}
const initialQuestionLoader = async () => {
if (!initialLoadQuestions) {
// Mark that we've done the initial load
setInitialLoadQuestions(true);
//NOTE: Adjusting for different definition of viewof
// Programmatically set the questions input’s value
questionsElement.value = await loadQuestions(
settings.questions[settings.questions.length - 1]
);
// Notify the reactive runtime that the input changed
questionsElement.dispatchEvent(new Event("input", {bubbles: true}));
}
display("Initial Question Loader");
};
Import external CSV of questions
//!!!!!!!!!!!!!!!!!
// NOTE: Need to re-work this as an async function due to await
//!!!!!!!!!!!!!!!!!
const onQuestionUpload = async () => {
//viewof questions.value = csvToQuestions(await questionUpload.csv());
//!!!! Experimental
//questions.value = csvToQuestions(await questionUpload.csv());
questionsElement.value = csvToQuestions(await questionUpload.csv());
//viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
//!!!! Experimental
//questions.dispatchEvent(new Event('input', {bubbles: true}));
questionsElement.dispatchEvent(new Event('input', {bubbles: true}));
};
display(onQuestionUpload)
function csvToQuestions(csv) {
return csv.reduce(
(acc, row) => {
// Now append the rows question attributes and values to the current question being processed
const attribute = row['key'];
const value = row['value'];
const id = row['id'] || acc.previous?.id;
let current = acc.previous;
if (id != acc.previous?.id) {
current = {
id: id
}
acc.questions.push(current)
}
const arrays = ['options', 'rows', 'columns'];
if (arrays.some(arr => attribute.startsWith(arr))) {
// But if the element is packed as an array we don't unwind
let packed = false;
try {
if (Array.isArray(eval(value))) {
packed = true;
current[attribute] = value;
}
} catch (err) {}
if (attribute === 'rows' && !Number.isNaN(+value)) {
// When rows is in a textarea is it not in an array
current[attribute] = value;
} else if (!packed) {
// Arrays come in a list of elements
const array = current[attribute] || [];
if (arrays.includes(attribute)) {
array.push({
value: value,
label: value
});
} else {
array.push(value);
}
current[attribute] = array;
}
} else {
current[attribute] = sanitizeValue(value);
}
return {
questions: acc.questions,
previous: current
}
}
, {
questions: [],
previous: null
}
).questions.reduce( // Index by id
(map, q) => {
const {id, ...value} = q
map.set(id, value)
return map;
},
new Map() // Map remembers insertion order which is useful
)
}
Export questions to CSV
const updateQuestionsCsvDataUriView = () => {
//viewof questionsCsvDataUriView.value = questionsCsvDataUri /* sync questionsCsvDataUri changes to the view */
//NOTE: Adjusting for different definition of viewof
//questionsCsvDataUriView.value = questionsCsvDataUri /* sync
questionsCsvDataUriViewElement.value = questionsCsvDataUri /* sync questionsCsvDataUri changes to the view */
//viewof questionsCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
//NOTE: Adjusting for different definition of viewof
//questionsCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
questionsCsvDataUriViewElement.dispatchEvent(new Event('input', {bubbles: true}))
}
SyntaxError: Unexpected token (1:55)
const downloadQuestionsCsv = htl.html`<a href=${viewof questionsCsvDataUriView.value} download="questions_${Date.now()}.csv">
Download questions.csv
</a>
${exportQuestionsProblems.length > 0 ? md`<mark> Warning, some questions are not exporting properly, you may lose data in export` : null}
`
SyntaxError: Unexpected keyword 'const' (2:2)
Layout
Import layout from CSV
const onLayoutUpload = async() => {
//viewof layoutData.value = {data: csvToLayout(await layoutUpload.csv())}
// NOTE: Changing due to re-definition of viewof
// layoutData.value = {data: csvToLayout(await layoutUpload.csv())}
layoutDataElement.value = {data: csvToLayout(await layoutUpload.csv())}
//viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
// NOTE: Changing due to re-definition of viewof
// layoutData.dispatchEvent(new Event('input', {bubbles: true}))
layoutDataElement.dispatchEvent(new Event('input', {bubbles: true}))
};
display(onLayoutUpload)
Export layout to CSV
const layoutCsvDataUri = URL.createObjectURL(new Blob([ d3.csvFormat(exportLayoutCSV) ], { type: 'text/csv' }));
//viewof layoutCsvDataUriView = Inputs.input(undefined)
const layoutCsvDataUriViewElement = Inputs.input(undefined);
const layoutCsvDataUriView = Generators.input(layoutCsvDataUriViewElement);
display(layoutCsvDataUriViewElement)
const updateLayoutCsvDataUriView = () => {
//viewof layoutCsvDataUriView.value = layoutCsvDataUri
// Note: Adjusting due to different definition of viewof
//layoutCsvDataUriView.value = layoutCsvDataUri
layoutCsvDataUriViewElement.value = layoutCsvDataUri
//viewof layoutCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
// Note: Adjusting due to different definition of viewof
//layoutCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
layoutCsvDataUriViewElement.dispatchEvent(new Event('input', {bubbles: true}))
}
SyntaxError: Unexpected token (1:52)
const downloadLayoutCsv = htl.html`<a href=${viewof layoutCsvDataUriView.value} download="layout_${Date.now()}.csv">
Download layout.csv
</a>
${exportLayoutProblems.length > 0 ? md`<mark> Warning, some layouts are not exporting properly, you may lose data in export` : null}
`
//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!
//!!!! NOTE: This does not appear to have an appropriate setter.
//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!
// NOTE: Adjusting this to define a Mutable and a setter function
const initialLoadLayout = Mutable(false)
//const initialLayoutLoader = {
// if (!initialLoadLayout) {
// mutable initialLoadLayout = true;
// setLayout([...await loadLayout(settings.layout[settings.layout.length - 1])])
// }
// return "Initial Layout Loader"
//}
const initialLayoutLoader = async () => {
if (!initialLoadLayout.value) {
initialLoadLayout.value = true; // mark as loaded
const latestLayoutKey = settings.layout[settings.layout.length - 1];
const layout = await loadLayout(latestLayoutKey);
setLayout([...layout]);
}
return "Initial Layout Loader";
};
function setLayout(data) {
const choices = learnChoices(data);
menuOptions.data = choices["menu"]
setOptions.data = choices["set"]
//viewof menuOptions.dispatchEvent(new Event('input', {bubbles: true}))
menuOptions.dispatchEvent(new Event('input', {bubbles: true}))
//viewof setOptions.dispatchEvent(new Event('input', {bubbles: true}))
setOptions.dispatchEvent(new Event('input', {bubbles: true}))
layoutData.data = data;
//viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
layoutData.dispatchEvent(new Event('input', {bubbles: true}))
}
const learnChoices = (data) => {
const columns = ["menu", "set"]
const counts = data.reduce(
(arr, l) => {
columns.forEach(c => {
arr[c] = arr[c] || {};
arr[c][l[c]] = (arr[c][l[c]] || 0) + 1
})
return arr;
}, {})
return Object.fromEntries(Object.entries(counts).map(([key, counts]) => {
return [key, Object.keys(counts).map(k => ({[key]: k}))]
}))
}
Export UI
const sampleExportUi = exportUi()
SyntaxError: Unexpected token (6:21)
const exportUi = () => {
const now = Date.now();
return viewUI`<div class="space-y-3">
<div>
<a href=${viewof questionsCsvDataUriView.value} download="questions_${Date.now()}.csv">Download Questions</a>
</div>
<div>
<a href=${viewof layoutCsvDataUriView.value} download="layout_${Date.now()}.csv">Download Layout</a>
</div>
</div>`
}
Import UI
//viewof sampleImportUi = importUi()
const sampleImportUiElement = importUi();
const sampleImportUi = Generators.input(sampleImportUiElement);
display(sampleImportUiElement)
sampleImportUi
SyntaxError: Unexpected token (5:13)
const importUi = (afterSave) => {
const submitFiles = async () => {
if (ui.value.questionsCsv) {
console.log('Updating questions CSV')
viewof questions.value = csvToQuestions(await ui.value.questionsCsv.csv());
}
if (ui.value.layoutCsv) {
console.log('Updating layout CSV')
viewof layoutData.value = {data: csvToLayout(await ui.value.layoutCsv.csv())}
}
if (ui.value.questionsCsv || ui.value.layoutCsv) {
viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
}
if (typeof afterSave === 'function') {
afterSave();
}
}
const submit = Inputs.button(buttonLabel({label: "Save"}), {reduce: submitFiles});
const ui = viewUI`<div class="space-y-3">
<h3 class="f5">Questions CSV file</h3>
${['questionsCsv', fileInput({prompt: "Drop questions as a CSV file here"})]}
<h3 class="f5">Layout CSV file</h3>
${['layoutCsv', fileInput({prompt: "Drop layout as a CSV file here"})]}
<div>
${submit}
</div>`
return ui;
}
Edit user choices within the layout Editors
function selection(title) {
const choices = dataEditor([], {
columns: [title],
width: {
[title]: "400px"
},
format: {
[title]: (d) => Inputs.text({value: d, width: "400px", disabled: true}),
},
})
const addForm = viewUI`<div style="display: flex;">
${[title, Inputs.text({
label: `Add ${title}`
})]}
${Inputs.button("add", {
reduce: () => {
choices.value.data = [...choices.value.data, addForm.value]
addForm[title].value = '';
choices.dispatchEvent(new Event('input', {bubbles: true}));
}
})}
`
return viewUI`<div><details>
<summary>Edit choices for <b>${title}</b></summary>
${['...', choices]}${cautious(() => addForm)}
`
}
//viewof menuOptions = selection("menu")
const menuOptionsElement = selection("menu");
const menuOptions = Generators.input(menuOptionsElement);
display(menuOptionsElement)
//viewof setOptions = selection("set")
const setOptionsElement = selection("set");
const setOptions = Generators.input(setOptionsElement);
display(setOptionsElement)
Layout Data\
SyntaxError: Unexpected token (1:7)
viewof layout = Inputs.input(layoutData)
/*
{
const menuOptionsArr = menuOptions.data.map(d => d["menu"]);
const setOptionsArr = setOptions.data.map(d => d["set"]);
return dataEditor(layoutData.data, {
columns: ["id", "menu", "set", "role"],
format: {
"id": (d) => Inputs.text({value: d}),
"menu": (d) => Inputs.select(menuOptionsArr, {value: d}),
"set": (d) => Inputs.select(setOptionsArr, {value: d}),
"role": (d) => Inputs.text({value: d})
},
width: {
"set": "100px"
},
tableClass: "layout",
stylesheet: `
.layout .col-cell_name form {
width: 300px
}
.layout .col-menu form {
width: 100px
}
.layout .col-set form {
width: 100px
}
`
})
}*/
Data Quality Checks
Questions that have no layout
SyntaxError: 'return' outside of function (4:4)
{
const results = [...questionsNoLayout.entries()].map(([k, v]) => ({id: k, ...v}));
if (results.length > 0) {
return dataEditor(results, {
columns: ["id", "type"]
})
} else {
return md`✅ The are no questions with no layout`
}
}
questionsNoLayout.values().next()
Layouts with no question\
SyntaxError: 'return' outside of function (4:4)
{
const results = layoutsNoQuestion.map(([name, layoutArray]) => layoutArray[0]);
if (results.length > 0) {
return dataEditor(results)
} else {
return md`✅ The are no layouts with no questions`
}
}
Questions with options but some of the options do not have a 'value'
All options need value's defined, this is the key used to ensure updates to question text do not affect the endusers pre-existing answers.
SyntaxError: 'return' outside of function (3:4)
{
if (optionsWithoutValue.length > 0) {
return md`⚠️ There are ${optionsWithoutValue.length} mistakes
${optionsWithoutValue.map(mistake => `\n- ${mistake[1].cell_name}`)}`
} else {
return md`✅ All options have a value defined`
}
}
const optionsWithoutValue = [...surveyOutput.questions.entries()].map(([k, q]) => [k, reifyAttributes(q)]).filter(([name, question]) => question.options && !question.options.some(option => option.value))
const layoutById = d3.group(layout.data, d => d.id)
const duplicateLayouts = Object.fromEntries([...layoutById.entries()].filter(([name, layoutArr]) => layoutArr.length > 1))
const questionsNoLayout = new Map([...questions.entries()].filter(([name, q]) => !layoutById.has(name)))
const layoutsNoQuestion = ([...layoutById.keys()].filter(name => !surveyOutput.questions.has(name))).map(k => [k, layoutById.get(k)])
const exportLayoutCSV = surveyOutput.layout
SyntaxError: Unexpected keyword 'const' (2:2)
const exportLayoutProblems = {
const exportedLayout = csvToLayout(exportLayoutCSV);
const problems = [];
for (var i = 0; i < Math.max(surveyOutput.layout.length, exportedLayout.length); i++) {
if (!_.isEqual(layout.data[i], exportedLayout[i])) {
problems.push({
row: i,
layout: layout.data[i],
exportedLayout: exportedLayout[1]
})
}
}
return problems;
}
Config
Config is additional data that might be useful such as the menu display titles.
//viewof latestConfig = editor({
const latestConfigElement = editor({
type: "object",
title: "Config",
properties: {
pageTitle: {
type: "string"
},
menuSegmentLabels: {
type: "object",
additionalProperties: { type: "string" }
}
}
}, {
theme: "spectre",
disable_edit_json: true,
disable_properties: false,
iconlib: "spectre",
show_errors: "always",
prompt_before_delete: "false"
});
const latestConfig = Generators.input(latestConfigElement);
display(latestConfigElement)
//!!!!!!!!!!!!!!!!!
// NOTE: Re-worked as an async function (no generator) due to await
//!!!!!!!!!!!!!!!!!
// NOTE: Original (Observable notebook style):
// viewof save_config = async function* () {
// if (viewof surveyConfig.value && !_.isEqual(latestConfig, viewof surveyConfig.value)) {
// yield md`Saving...`
// viewof surveyConfig.value = latestConfig;
// await saveConfig(latestConfig);
// await files.save("settings.json", viewof settings.value);
// yield md`saved`
// } else {
// yield md`no changes`
// }
// }
//!! This is an experimental departure. Check.
const save_config = async () => {
//if (viewof surveyConfig.value && !_.isEqual(latestConfig, viewof surveyConfig.value)) {
// NOTE: Adjusting due to different definition of viewof
// NOTE: Using surveyConfigElement instead of viewof surveyConfig
if (surveyConfigElement.value && !_.isEqual(latestConfig, surveyConfigElement.value)) {
//yield md`Saving...`
// NOTE: No yield here; caller is responsible for showing "Saving..." state
//viewof surveyConfig.value = latestConfig;
// NOTE: Changing due to different definition of viewof
surveyConfigElement.value = latestConfig;
await saveConfig(latestConfig);
//await files.save("settings.json", viewof settings.value);
// NOTE: Changing due to different definition of viewof
await files.save("settings.json", settings.value);
//yield md`saved`
// NOTE: Return status instead of yielding markdown
return "saved";
} else {
//yield md`no changes`
// NOTE: Return status instead of yielding markdown
return "no changes";
}
};
//viewof surveyConfig = Inputs.input()
const surveyConfigElement = Inputs.input();
const surveyConfig = Generators.input(surveyConfigElement);
display(surveyConfigElement)
const sync_ui = () => {
//viewof latestConfig.value = surveyConfig
// latestConfig.value = surveyConfig
// !!!!!!!!!!!!!
// NOTE: Check if this works
latestConfigElement.value = surveyConfig
}
const load_config = async () => {
//viewof surveyConfig.value = settings.configs?.length > 0
// Note: Adjusting due to different definition of config element
//surveyConfig.value = settings.configs?.length > 0
surveyConfigElement.value = settings.configs?.length > 0
? await loadConfig(settings.configs[settings.configs.length - 1])
: {};
//viewof surveyConfig.dispatchEvent(new Event('input', {bubbles: true}))
// Note: Adjusting due to different definition of config element
//surveyConfig.dispatchEvent(new Event('input', {bubbles: true}))
surveyConfigElement.dispatchEvent(new Event('input', {bubbles: true}))
}
//import {editor} from "@a10k/hello-json-editor"
import {editor} from "/components/hello-json-editor.js"
display(editor)
Styles
SyntaxError: Assignment to external variable 'styles' (1:0)
styles = html`<style>
/* Survey Editor */
.survey-editor__import,
.survey-editor__export,
.survey-editor__editor {
display: none;
}
[data-survey-editor-state="import"] .survey-editor__import,
[data-survey-editor-state="export"] .survey-editor__export,
[data-survey-editor-state="editor"] .survey-editor__editor {
display: block;
}
/* Styles when displayed as a stand alone notebook */
[data-standalone-designer-notebook] .observablehq > h2 {
padding-top: var(--spacing-medium);
border-top: 1px solid;
border-color: #eee; /* .b--light-gray */
}
</style>`
Styles for the in notebook demo
<style>
.survey-ui {
overflow-y: auto;
max-height: 600px;
overscroll-behavior-y: contain;
}
</style>
const surveyPreviewTitle = md`## Survey Preview`
//viewof responses = {
const responsesElement = (() => {
addMenuBehaviour;
const view = surveyView(
surveyOutput.questions,
surveyOutput.layout,
surveyOutput.config,
new Map(),
{
putFile: (name) => console.log("mock save " + name),
getFile: (name) => console.log("mock get " + name)
}
);
return view;
})();
const responses = Generators.input(responsesElement);
display(responsesElement)
``
responses
import {surveyView, addMenuBehaviour} from '@categorise/surveyslate-styling'
Preview Answers
responses
Revert changes
Rollback survey to last deployed version
//viewof reollbackButton = Inputs.button("revert", {
const reollbackButtonElement = Inputs.button("revert", {
reduce: async () => {
await revertChanges();
}
});
const reollbackButton = Generators.input(reollbackButtonElement);
display(reollbackButtonElement)
const deployTitle = md`## Deploy Survey Version`
Last deployed: SyntaxError: invalid expression
Cloud Configuration
login
const me = getUser()
const myTags = listUserTags(me.UserName)
const REGION = 'us-east-2'
//import {listObjects, getObject, putObject, listUsers, createUser, deleteUser, getUser, listAccessKeys, createAccessKey, deleteAccessKey, viewof manualCredentials, viewof mfaCode, saveCreds, listUserTags, tagUser, untagUser, iam, s3, listGroups, listGroupsForUser, addUserToGroup, removeUserFromGroup} with {REGION as REGION} from '@tomlarkworthy/aws'
import {listObjects, getObject, putObject, listUsers, createUser, deleteUser, getUser, listAccessKeys, createAccessKey, deleteAccessKey, manualCredentials, manualCredentialsElement, mfaCode, saveCreds, saveCredsElement, listUserTags, tagUser, untagUser, iam, s3, listGroups, listGroupsForUser, addUserToGroup, removeUserFromGroup, applyCredentials} from '/components/aws.js'
md`---
## Helpers`
const sanitizeValue = (text) => {
text = text.trim()
if (text === "TRUE") return true;
if (text === "FALSE") return false;
return text
}
Dependencies
//import {createQuestion, reifyAttributes, bindLogic, setTypes, config} from '@categorise/survey-components'
import {createQuestion, reifyAttributes, bindLogic, setTypes, config} from '/components/survey-components.js'
//import {toc} from "@bryangingechen/toc"
import {toc} from "/components/toc.js"
//import {view, cautious} from '@tomlarkworthy/view'
import {viewUI, cautious} from '/components/view.js'
//import {fileInput} from "@tomlarkworthy/fileinput"
import {fileInput} from "/components/fileinput.js"
//import {dataEditor} from '@tomlarkworthy/dataeditor'
import {dataEditor} from '/components/dataeditor.js'
//import {pageHeader, pageFooter, buttonLabel} from "@categorise/common-components"
import {pageHeader, pageFooter, buttonLabel} from "/components/common-components.js"
//import {localStorageView} from '@tomlarkworthy/local-storage-view'
import {localStorageView} from '/components/local-storage-view.js';
display(localStorageView)
//import { substratum } from "@categorise/substratum"
//substratum({ invalidation })