diff --git a/docz/docs/03-demos/02-grid/16-rdg.md b/docz/docs/03-demos/02-grid/16-rdg.md
index 39ae362..e07d6fd 100644
--- a/docz/docs/03-demos/02-grid/16-rdg.md
+++ b/docz/docs/03-demos/02-grid/16-rdg.md
@@ -9,12 +9,11 @@ import CodeBlock from '@theme/CodeBlock';
 
 :::note
 
-This demo was last tested on 2023 April 18 with `react-data-grid 7.0.0-beta.28`,
-`create-react-app` 5.0.1 and React 18.2.0.
+This demo was last tested on 2023 September 17 with `react-data-grid` version
+`7.0.0-beta.39` and React 18.2.0.
 
 :::
 
-
 The demo creates a site that looks like the screenshot below:
 
 
@@ -117,7 +116,7 @@ cd sheetjs-rdg
 2) Install dependencies:
 
 {`\
-npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz react-data-grid`}
+npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz react-data-grid@7.0.0-beta.39`}
 
 
 3) Download [`App.tsx`](pathname:///rdg/App.tsx) and replace `src/App.tsx`.
@@ -126,5 +125,15 @@ npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz react-data-
 curl -L -o src/App.tsx https://docs.sheetjs.com/rdg/App.tsx
 ```
 
-4) run `npm start`.  When you load the page in the browser, it will attempt to
-   fetch  and load the data.
+4) Start the development server:
+
+```bash
+npm start
+```
+
+#### Testing
+
+5) When the page loads, it will fetch , parse
+with SheetJS, and load the data in the data grid.
+
+6) Click one of the "export" buttons to export the grid data to file.
diff --git a/docz/docs/03-demos/05-mobile/03-quasar.md b/docz/docs/03-demos/05-mobile/03-quasar.md
index c9d22c6..18c1c2c 100644
--- a/docz/docs/03-demos/05-mobile/03-quasar.md
+++ b/docz/docs/03-demos/05-mobile/03-quasar.md
@@ -1,5 +1,7 @@
 ---
-title: Quasar
+title: Data in Quasar Apps
+sidebar_label: Quasar
+description: Build data-intensive mobile apps with Quasar and Cordova. Seamlessly integrate spreadsheets into your app using SheetJS. Let data in your Excel spreadsheets shine.
 pagination_prev: demos/static/index
 pagination_next: demos/desktop/index
 sidebar_position: 3
@@ -10,10 +12,17 @@ sidebar_custom_props:
 import current from '/version.js';
 import CodeBlock from '@theme/CodeBlock';
 
-The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
-from the main entrypoint or any script in the project.
+[Quasar](https://quasar.dev/) is a VueJS framework for building iOS and Android
+apps with the Cordova platform.
 
-The "Complete Example" creates an app that looks like the screenshots below:
+[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
+data from spreadsheets.
+
+This demo uses Quasar and SheetJS to process data and generate spreadsheets.
+We'll explore how to load SheetJS in an Quasar app and use Quasar and Cordova
+features to extract data from, and write data to, spreadsheets on the device.
+
+The ["Demo"](#demo) creates an app that looks like the screenshots below:
 
 
   | iOS@@ -30,10 +39,14 @@ The "Complete Example" creates an app that looks like the screenshots below:
 
 ### Integration Details
 
+The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
+from the main entrypoint or any script in the project.
+
 This demo will use the Quasar ViteJS starter project with VueJS and Cordova.
+The starter places the backing Cordova project in the `src-cordova` folder.
 
 The complete solution uses `cordova-plugin-file` for file operations.  It can
-be installed like any other Cordova plugin:
+be installed from the Cordova folder:
 
 ```bash
 cd src-cordova
@@ -41,15 +54,21 @@ cordova plugin add cordova-plugin-file
 cd ..
 ```
 
-#### Reading data
+### Reading data
 
-The `q-file` component presents an API reminiscent of File Input elements:
+The QFile[^1] component presents an API reminiscent of File Input elements:
 
 ```html
 
 ```
 
-When binding to the `input` element, the callback receives an `Event` object:
+When binding to the `input` element, the callback receives an `Event` object.
+Using standard DOM operations, the file data can be pulled into an `ArrayBuffer`
+and parsed using the SheetJS `read` method[^2]. `read` returns a workbook[^3]
+object that holds data and metadata for each worksheet.
+
+This snippet reads a workbook, pulls the first worksheet, generates an array of
+objects using the SheetJS `sheet_to_json`[^4] method, and updates state:
 
 ```ts
 import { read } from 'xlsx';
@@ -74,14 +93,33 @@ async function updateFile(v) { try {
 
 #### Writing data
 
-The API is shaped like the File and Directory Entries API.  For clarity, since
-the code is a "pyramid of doom", the error handlers are omitted:
+Starting from an array of objects, the SheetJS `json_to_sheet` method[^5]
+generates a SheetJS worksheet object. The `book_append_sheet` and `book_new`
+helper functions[^6] create a SheetJS workbook object that can be exported:
 
-```ts
+```js
+import { utils } from 'xlsx';
+
+// assuming `todos` is a VueJS ref whose value is an array of objects
+const ws = utils.json_to_sheet(todos.value);
+const wb = utils.book_new();
+utils.book_append_sheet(wb, ws, "SheetJSQuasar");
+```
+
+The SheetJS `write` function[^7] with the option `type: "buffer"` will generate
+`Uint8Array` objects that can be converted to blobs and exported:
+
+```js
 import { write } from 'xlsx';
 
 // on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array`
 const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"});
+```
+
+The `cordova-plugin-file` API writes the data to the filesystem. The code uses
+the File and Directory Entries API[^8]:
+
+```ts
 // Request filesystem access for persistent storage
 window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
   // Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary
@@ -107,12 +145,11 @@ window.requestFileSystem(window.PERSISTENT, 0, function(fs) {
 
 :::note
 
-This demo was tested on an Intel Mac on 2023 April 08. `create-quasar@1.1.2`
-was installed during app creation.  The app used Quasar version `2.11.10`.
+The Android demo was last tested on 2023 September 18 with Quasar `2.12.7` on an
+emulated Pixel 3 + Android 13 ("Tiramisu") API 33.
 
-The iOS simulator runs iOS 16.2 on an iPhone 14 Pro Max.
-
-The Android simulator runs Android 12.0 (S) API 31 on a Pixel 3.
+The iOS demo was last tested on 2023 September 18 with Quasar `2.12.7` on an
+emulated iPhone SE (3rd generation) + iOS 16.4.
 
 :::
 
@@ -196,20 +233,38 @@ Return to the project directory:
 cd ..
 ```
 
-4) Start the development server:
+11) Enable file sharing and make the documents folder visible in the iOS app.
+The following lines must be added to `src-cordova/platforms/ios/SheetJSQuasar/SheetJSQuasar-Info.plist`:
+
+```xml title="src-cordova/platforms/ios/SheetJSQuasar/SheetJSQuasar-Info.plist (add to file)"
+
+
+
+  UIFileSharingEnabled
+  
+  LSSupportsOpeningDocumentsInPlace
+  
+
+  CFBundleDevelopmentRegion
+```
+
+(The root element of the document is `plist` and it contains one `dict` child)
+
+
+5) Start the development server:
 
 ```bash
 quasar dev -m ios
 ```
 
-:::caution
+:::caution pass
 
 If the app is blank or not refreshing, delete the app and close the simulator,
 then restart the development process.
 
 :::
 
-5) Add the Dialog plugin to `quasar.config.js`:
+6) Add the Dialog plugin to `quasar.config.js`:
 
 ```js title="quasar.config.js"
     framework: {
@@ -221,7 +276,7 @@ then restart the development process.
     },
 ```
 
-6) In the template section of `src/pages/IndexPage.vue`, replace the example
+7) In the template section of `src/pages/IndexPage.vue`, replace the example
    with a Table, Save button and Load file picker component:
 
 ```html title="src/pages/IndexPage.vue"
@@ -266,7 +321,7 @@ then restart the development process.
 
 :::
 
-7) Wire up the `updateFile` function:
+8) Wire up the `updateFile` function:
 
 ```ts title="src/pages/IndexPage.vue"
 import { defineComponent, ref } from 'vue';
@@ -315,7 +370,7 @@ To test that reading works:
 
 Once selected, the screen should refresh with new contents.
 
-8) Wire up the `saveFile` function:
+9) Wire up the `saveFile` function:
 
 ```ts title="src/pages/IndexPage.vue"
     function saveFile() {
@@ -401,7 +456,7 @@ id,content
 
 **Android**
 
-9) Create the Android project:
+10) Create the Android project:
 
 ```bash
 cd src-cordova
@@ -409,24 +464,12 @@ cordova platform add android
 cd ..
 ```
 
-10) Start the simulator:
+11) Start the simulator:
 
 ```bash
 quasar dev -m android
 ```
 
-:::note
-
-In local testing, the Quasar build process threw an error:
-
-```
-  java.lang.IllegalArgumentException: Unsupported class file major version 63
-```
-
-This was resolved by rolling back to Java 1.8
-
-:::
-
 To test that reading works:
 
 - Click and drag `pres.numbers` from a Finder window into the simulator.
@@ -442,3 +485,12 @@ To test that writing works:
 adb exec-out run-as org.sheetjs.quasar cat files/files/SheetJSQuasar.xlsx > /tmp/SheetJSQuasar.xlsx
 npx xlsx-cli /tmp/SheetJSQuasar.xlsx
 ```
+
+[^1]: See ["File Picker"](https://quasar.dev/vue-components/file-picker) in the Quasar documentation.
+[^2]: See [`read` in "Reading Files"](/docs/api/parse-options)
+[^3]: See ["SheetJS Data Model"](/docs/csf/) for more details on workbooks, worksheets, and other concepts.
+[^4]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
+[^5]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
+[^6]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
+[^7]: See [`write` in "Writing Files"](/docs/api/write-options)
+[^8]: See [`requestFileSystem`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestFileSystem) in the MDN Web Docs for more details.
\ No newline at end of file
diff --git a/docz/docs/03-demos/07-data/index.md b/docz/docs/03-demos/07-data/index.md
index 100d0e1..8deacaa 100644
--- a/docz/docs/03-demos/07-data/index.md
+++ b/docz/docs/03-demos/07-data/index.md
@@ -183,7 +183,9 @@ The following Web APIs are featured in separate demos:
     {item.label}{item.customProps?.summary && (" - " + item.customProps.summary)}
   );
 })}
-IndexedDB API
+Local Storage API
+IndexedDB API
+
 
 ### SQL Databases
 
diff --git a/docz/docs/03-demos/07-data/02-storageapi.md b/docz/docs/03-demos/08-local/03-storageapi.md
similarity index 93%
rename from docz/docs/03-demos/07-data/02-storageapi.md
rename to docz/docs/03-demos/08-local/03-storageapi.md
index bfadf26..fb5635b 100644
--- a/docz/docs/03-demos/07-data/02-storageapi.md
+++ b/docz/docs/03-demos/08-local/03-storageapi.md
@@ -1,9 +1,9 @@
 ---
 title: Local Storage API
-pagination_prev: demos/desktop/index
-pagination_next: demos/local/index
+pagination_prev: demos/data/index
+pagination_next: demos/cloud/index
 sidebar_custom_props:
-  type: web
+  summary: Reading and writing data in an in-browser Key-Value store
 ---
 
 The Storage API, encompassing `localStorage` and `sessionStorage`, describes
@@ -14,6 +14,17 @@ This demo covers two common use patterns:
 - "Row Objects" shows a simple convention for loading and storing row objects
 - "Simple Strings" discusses how to persist and recover a raw Storage
 
+:::note
+
+Each browser demo was tested in the following environments:
+
+| Browser     | Date       |
+|:------------|:-----------|
+| Chrome 116  | 2023-09-17 |
+| Safari 16.6 | 2023-09-17 |
+
+:::
+
 ## Row Objects
 
 Consider the following array of objects of data:
@@ -70,12 +81,6 @@ function localStorage_to_sheet() {
 
 ### Live Demo
 
-:::note
-
-This demo was last tested on 2023 April 09.
-
-:::
-
 This example will fetch , fill `localStorage`
 with rows, then generate a worksheet from the rows and write to a new file.
 
@@ -84,13 +89,15 @@ After saving the exported file, the Local Storage can be inspected in the
 
 
 
-:::caution
+:::caution pass
 
 This example is for illustration purposes. If array of objects is available, it
 is strongly recommended to convert that array to a worksheet directly.
 
 :::
 
+ | Live Demo (click to show)
+
 ```jsx live
 function SheetJStorage() {
   const [url, setUrl] = React.useState("https://sheetjs.com/pres.numbers");
@@ -129,6 +136,8 @@ function SheetJStorage() {
 }
 ```
 
+Live Demo (click to show)
+
 ```jsx live
 function SheetJSRandomStorage() {
   const [out, setOut] = React.useState("");
@@ -231,3 +236,5 @@ function SheetJSRandomStorage() {
   > );
 }
 ```
+
+Code (click to show)
+- "Exporting files": SheetJS libraries will read XLSX and ODS files exported by
+Google Sheets and generate CSV rows from every worksheet.
 
-```js title=common.js
-const fs = require("fs");
-const { GoogleSpreadsheet } = require('google-spreadsheet');
+:::warning pass
 
-module.exports = async(ID) => {
-  /* get credentials */
-  const creds = JSON.parse(fs.readFileSync('key.json'));
+It is strongly recommended to create a new Google account for testing.
 
-  /* initialize sheet and authenticate */
-  const doc = new GoogleSpreadsheet(ID);
-  await doc.useServiceAccountAuth(creds);
-  await doc.loadInfo();
-  return doc;
-}
+**One small mistake could result in a block or ban from Google services.**
+
+:::
+
+:::caution pass
+
+Google Sheets deprecates APIs quickly and there is no guarantee that the
+referenced APIs will be available in the future.
+
+:::
+
+## Integration Details
+
+This demo uses the following NodeJS modules:
+
+- `google-auth-library`[^1] to authenticate with Google APIs
+- `node-google-spreadsheet`[^2] to interact with Google Sheets v4 API
+
+:::info Initial Setup
+
+There are a number of steps to enable the Google Sheets API and Google Drive API
+for an account. The [Complete Example](#complete-example) covers the process.
+
+:::
+
+### Authentication
+
+It is strongly recommended to use a service account for Google API operations.
+The ["Service Account Setup" section](#service-account-setup) covers how to
+create a service account and generate a JSON key file.
+
+```js title="Authenticate using a JSON key file"
+import { JWT } from 'google-auth-library'
+import { GoogleSpreadsheet } from 'google-spreadsheet';
+
+// adjust the path to the actual key file.
+// highlight-next-line
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+const jwt = new JWT({
+  email: creds.client_email,
+  key: creds.private_key,
+  scopes: [
+    'https://www.googleapis.com/auth/spreadsheets',
+    'https://www.googleapis.com/auth/drive.file',
+  ]
+});
 ```
 
-How to run locally (click to show)
-
-0) Follow the [Initial Configuration](#initial-configuration).
-
-1) Create a new Google Sheet and share with the generated service account.  It
-should be granted the "Editor" role
-
-2) Install the dependencies:
-
-{`\
-npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz google-spreadsheet@3.3.0`}
-
-
-2) Save the following snippet to `common.js`:
-
-```js title=common.js
-const fs = require("fs");
-const { GoogleSpreadsheet } = require('google-spreadsheet');
-
-module.exports = async(ID) => {
-  /* get credentials */
-  const creds = JSON.parse(fs.readFileSync('key.json'));
-
-  /* initialize sheet and authenticate */
-  const doc = new GoogleSpreadsheet(ID);
-  await doc.useServiceAccountAuth(creds);
-  await doc.loadInfo();
-  return doc;
-}
-```
-
-3) Save the following snippet to `pull.js`:
-
-```js title=pull.js
-const XLSX = require("xlsx");
-
-/* create a blank workbook */
-const wb = XLSX.utils.book_new();
-
-const init = require("./common");
-const ID = "";
-
-(async() => {
-
-  const doc = await init(ID);
-
-  for(let i = 0; i < doc.sheetsByIndex.length; ++i) {
-    const sheet = doc.sheetsByIndex[i];
-    const name = sheet.title;
-
-    /* get the header and data rows */
-    await sheet.loadHeaderRow();
-    const header = sheet.headerValues;
-    const rows = await sheet.getRows();
-    const aoa = [header].concat(rows.map(r => r._rawData));
-
-    /* generate a SheetJS Worksheet */
-    const ws = XLSX.utils.aoa_to_sheet(aoa);
-
-    /* add to workbook */
-    XLSX.utils.book_append_sheet(wb, ws, name);
-  }
-
-  /* write to SheetJS.xlsb */
-  XLSX.writeFile(wb, "SheetJS.xlsb");
-
-})();
-```
-
-4) Replace `` with the ID of the actual document.
-
-5) Run `node pull.js` once. It will create `SheetJS.xlsb`.
-
-6) Open `SheetJS.xlsb` and confirm the contents are the same as Google Sheets.
-
-7) Change some cells in the Google Sheets Document.
-
-8) Run `node pull.js` again and reopen `SheetJS.xlsb` to confirm value changes.
-
-How to run locally (click to show)
-
-0) Follow the [Initial Configuration](#initial-configuration).
-
-1) Create a new Google Sheet and share with the generated service account.  It
-should be granted the "Editor" role
-
-2) Install the dependencies:
-
-{`\
-npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz google-spreadsheet@3.3.0`}
-
-
-2) Save the following snippet to `common.js`:
-
-```js title=common.js
-const fs = require("fs");
-const { GoogleSpreadsheet } = require('google-spreadsheet');
-
-module.exports = async(ID) => {
-  /* get credentials */
-  const creds = JSON.parse(fs.readFileSync('key.json'));
-
-  /* initialize sheet and authenticate */
-  const doc = new GoogleSpreadsheet(ID);
-  await doc.useServiceAccountAuth(creds);
-  await doc.loadInfo();
-  return doc;
-}
-```
-
-3) Save the following snippet to `push.js`:
-
-```js title=push.js
-const XLSX = require("xlsx");
-const fs = require("fs");
-/* create dummy worksheet if `sheetjs.xlsx` does not exist */
-if(!fs.existsSync("sheetjs.xlsx")) {
-  const wb = XLSX.utils.book_new();
-  const ws1 = XLSX.utils.aoa_to_sheet([["a","b","c"],[1,2,3]]); XLSX.utils.book_append_sheet(wb, ws1, "Sheet1");
-  const ws2 = XLSX.utils.aoa_to_sheet([["a","b","c"],[4,5,6]]); XLSX.utils.book_append_sheet(wb, ws2, "Sheet2");
-  XLSX.writeFile(wb, "sheetjs.xlsx");
-}
-/* read and parse sheetjs.xlsx */
-const wb = XLSX.readFile("sheetjs.xlsx");
-
-const init = require("./common");
-const ID = "";
-
-(async() => {
-
-  const doc = await init(ID);
-
-  /* clear workbook */
-  {
-    /* delete all sheets after the first sheet */
-    const old_sheets = doc.sheetsByIndex;
-    for(let i = 1; i < old_sheets.length; ++i) {
-      await old_sheets[i].delete();
-    }
-    /* clear first worksheet */
-    old_sheets[0].clear();
-  }
-
-  /* write worksheets */
-  {
-    const name = wb.SheetNames[0];
-    const ws = wb.Sheets[name];
-    /* first worksheet already exists */
-    const sheet = doc.sheetsByIndex[0];
-
-    /* update worksheet name */
-    await sheet.updateProperties({title: name});
-
-    /* generate array of arrays from the first worksheet */
-    const aoa = XLSX.utils.sheet_to_json(ws, {header: 1});
-
-    /* set document header row to first row of the AOA */
-    await sheet.setHeaderRow(aoa[0])
-
-    /* add the remaining rows */
-    await sheet.addRows(aoa.slice(1));
-
-    /* the other worksheets must be created manually */
-    for(let i = 1; i < wb.SheetNames.length; ++i) {
-      const name = wb.SheetNames[i];
-      const ws = wb.Sheets[name];
-
-      const sheet = await doc.addSheet({title: name});
-      const aoa = XLSX.utils.sheet_to_json(ws, {header: 1});
-      await sheet.setHeaderRow(aoa[0])
-      await sheet.addRows(aoa.slice(1));
-    }
-  }
-
-})();
-```
-
-4) Replace `` with the ID of the actual document.
-
-5) Run `node push.js` once. It will create `sheetjs.xlsx` and update the sheet.
-
-6) Edit `sheetjs.xlsx` with some new data
-
-7) Run `node push.js` again and watch the Google Sheet update!
-
-