diff --git a/tools/admin/README.md b/tools/admin/README.md
new file mode 100644
index 0000000..1025d7e
--- /dev/null
+++ b/tools/admin/README.md
@@ -0,0 +1,293 @@
+# Site Admin Tool
+
+Comprehensive administration console for managing DA.live sites - library setup, integrations, and site configuration.
+
+## Features
+
+### Library Management
+- **Blocks** - Discover, document, and manage block components
+- **Templates** - Create and manage page templates
+- **Icons** - Manage SVG icons for your site
+- **Placeholders** - Define reusable text placeholders/tokens
+
+### Integrations
+- **AEM Assets** - Connect AEM as a Cloud Service assets repository
+- **Translation** - Configure translation services and behavior
+- **Universal Editor** - Set up WYSIWYG content authoring
+
+## Installation
+
+**Important:** Run these commands from your project root (where the `tools/` folder should be created or updated).
+
+### Option 1: Using npx (Recommended)
+
+```bash
+npx degit kmurugulla/brightpath/tools/admin tools/admin
+```
+
+### Option 2: Using curl
+
+```bash
+curl -L https://github.com/kmurugulla/brightpath/archive/refs/heads/main.tar.gz | \
+ tar -xz --strip=3 "brightpath-main/tools/admin" && \
+ mv admin tools/
+```
+
+### Option 3: Manual Copy
+
+Copy the entire `tools/admin` directory into your project's `tools/` folder.
+
+## Getting Started
+
+### Access the Tool
+
+Run your local development server:
+```bash
+aem up
+```
+
+Then access the admin tool via:
+```
+https://da.live/app/{org}/{site}/tools/admin/siteadmin?ref=local
+```
+
+Replace `{org}` and `{site}` with your DA.live organization and site names.
+
+**Query Parameters:**
+- `ref=local` - Points to your local development server (default port 3000)
+- `ref=main` - Points to your production site
+
+## Usage Guide
+
+### Library Setup - Initial Mode
+
+Create a new block library from scratch or add blocks to an existing library:
+
+**Steps:**
+1. **Enter GitHub Repository URL** - Provide the URL to your GitHub repository containing blocks
+ - Public repositories work immediately
+ - Private repositories require a GitHub token (will be prompted)
+ - Token can be saved securely for future use
+
+2. **Block Discovery** - Automatically discovers all blocks in your repository
+ - Searches for blocks at any level (supports nested structures)
+ - Detects new blocks not yet in your library (marked with "New" badge)
+ - Use "Select New Only" to quickly add just new blocks
+ - Use "Select All" / "Deselect All" for bulk operations
+
+3. **Block Analysis** - Each block is analyzed for:
+ - Structure and variants
+ - CSS classes and features
+ - Required vs. optional content
+ - Intelligent placeholder documentation generated
+
+4. **Sample Pages (Optional)** - Select pages to extract real content examples
+ - Browse your DA.live content via page picker
+ - Extract actual block usage from live pages
+ - Enriches documentation with real examples
+
+5. **Create Library** - Click "Set Up Library" to:
+ - Generate block documentation
+ - Create/update `blocks.json` configuration
+ - Register library in site configuration
+ - Automatically version existing docs before overwriting
+
+### Library Setup - Update Examples Mode
+
+Update existing block documentation with new content examples:
+
+**Steps:**
+1. **Switch to "Update Examples" mode** using the toggle
+2. **Enter Organization and Site** - Your existing DA.live site details
+3. **Select Blocks** - Choose which blocks to refresh
+4. **Add Sample Pages** - Select pages containing updated block examples
+5. **Update** - Click "Update Examples" to:
+ - Create version snapshots of existing docs
+ - Extract content from selected pages
+ - Update only the blocks found in those pages
+ - Preserve all other existing blocks
+
+### Managing Templates
+
+**Add Templates:**
+1. Navigate to "Templates" tab
+2. Enter template name (e.g., "Blog Post")
+3. Click "Select Page" to choose a source page
+4. Click "+ Add" to add to the list
+5. Click "Set Up Library" to save
+
+**Edit/Remove:**
+- Use "Edit" button on existing templates to modify
+- Use "Remove" button to delete from library
+
+### Managing Icons
+
+**Add Icons:**
+1. Navigate to "Icons" tab
+2. Enter icon name (e.g., "search", "menu")
+3. Click "Select Page" to choose an SVG file
+4. Click "+ Add" to add to the list
+5. Click "Set Up Library" to save
+
+**SVG Requirements:**
+- Must be valid SVG format
+- Recommended size: 24x24px
+- Clean, optimized paths
+
+### Managing Placeholders
+
+**Add Placeholders:**
+1. Navigate to "Placeholders" tab
+2. Enter a key (e.g., "copyright")
+3. Enter a value (e.g., "© 2024 Company Name")
+4. Click "+ Add"
+5. Click "Set Up Library" to save
+
+**Use Cases:**
+- Legal disclaimers
+- Copyright notices
+- Repeated text snippets
+- Dynamic content tokens
+
+### Integration Setup
+
+#### AEM Assets Integration
+
+Connect your AEM as a Cloud Service assets repository:
+
+1. Navigate to "AEM Assets" tab
+2. Enter **Repository ID** - Your AEM assets repository identifier
+3. Enter **Production Origin** - Your AEM production URL
+4. Click "Verify URL" to test the connection
+5. Select desired options:
+ - **Image Type** - Enable image type selection
+ - **Renditions Select** - Allow rendition selection
+ - **DM Delivery** - Dynamic Media delivery
+ - **Smart Crop Select** - Smart crop selection
+6. Click "Save Configuration"
+
+#### Translation Configuration
+
+Configure how translation services handle content:
+
+1. Navigate to "Translation" tab
+2. Configure settings:
+ - **Translate Behavior** - How to handle existing content (overwrite/skip)
+ - **Translate Staging** - Enable/disable staging
+ - **Rollout Behavior** - How to handle rollout (overwrite/skip)
+3. Click "Save Configuration"
+
+#### Universal Editor Setup
+
+Enable WYSIWYG content authoring:
+
+1. Navigate to "Universal Editor" tab
+2. Enter **Editor Path** - Path to your Universal Editor configuration
+3. Click "Save Configuration"
+
+## Architecture
+
+### File Structure
+```
+tools/admin/
+├── app/
+│ ├── handlers/ # Event handlers
+│ ├── main.js # Main application logic
+│ ├── router.js # Client-side routing
+│ ├── state.js # Application state
+│ └── templates.js # HTML templates
+├── operations/ # Business logic
+├── utils/ # Utility functions
+├── styles/ # CSS files
+│ ├── admin.css # Core styles (base, layout, nav)
+│ ├── blocks-section.css
+│ ├── library-items-section.css
+│ ├── integrations.css
+│ ├── progress.css
+│ ├── error-modal.css
+│ ├── page-picker.css
+│ └── github-section.css
+├── config.js # Configuration constants
+├── siteadmin.html # Entry point
+└── README.md
+```
+
+### Key Components
+
+- **Handlers** - Factory pattern for library items (templates, icons, placeholders)
+- **Operations** - GitHub API, DA.live API, library operations
+- **Utils** - Block analysis, content extraction, document generation
+- **State Management** - Centralized application state
+- **Router** - Hash-based client-side routing
+
+## Requirements
+
+### Access
+- Must be run from within DA.live for authentication
+- Write access to CONFIG for your organization
+ - [See permissions guide](https://docs.da.live/administrators/guides/permissions)
+
+### GitHub Integration
+- GitHub token needed **only** for private repositories
+- Token can be saved securely in browser for future use
+- Public repositories work without authentication
+
+### Optional
+- Sample pages for extracting real content examples
+- Tool generates intelligent placeholders without them
+
+## Troubleshooting
+
+### Authentication Issues
+- **Problem**: "DA.live authentication required"
+- **Solution**: Ensure you're accessing the tool from within DA.live, not directly via localhost
+
+### GitHub Rate Limiting
+- **Problem**: "GitHub API rate limit exceeded"
+- **Solution**: Add a GitHub token (increases rate limit from 60 to 5000 requests/hour)
+
+### Library Not Found
+- **Problem**: "No library found at this location"
+- **Solution**: Run "Library Setup" first in setup mode to create the library structure
+
+### Permission Errors
+- **Problem**: "Unable to update site configuration"
+- **Solution**: Ensure you have CONFIG write permissions for your organization
+
+## Development
+
+### Making Changes
+
+The tool is designed to be customized for your project:
+
+1. Edit files in `tools/admin/`
+2. Changes are reflected immediately with `ref=local`
+3. Test thoroughly before committing
+4. Run linting: `npm run lint`
+
+### Code Style
+
+- JavaScript: ES6+ with Airbnb ESLint configuration
+- CSS: Modern CSS with nesting, custom properties
+- No build step required - vanilla JavaScript
+
+### Contributing
+
+When modifying the tool:
+1. Follow existing code patterns
+2. Use the handler factory for new library item types
+3. Maintain CSS custom properties in `admin.css`
+4. Test all views and interactions
+5. Ensure linting passes
+
+## Support
+
+For issues or questions:
+- Check DA.live documentation: https://docs.da.live
+- Review this README
+- Check browser console for errors
+- Verify permissions and authentication
+
+## License
+
+MIT
diff --git a/tools/admin/app/handlers/aem-assets-handlers.js b/tools/admin/app/handlers/aem-assets-handlers.js
new file mode 100644
index 0000000..151bbf2
--- /dev/null
+++ b/tools/admin/app/handlers/aem-assets-handlers.js
@@ -0,0 +1,103 @@
+export function attachAemAssetsListeners(app, state) {
+ const repositoryIdInput = document.getElementById('aem-repository-id');
+ const prodOriginInput = document.getElementById('aem-prod-origin');
+ const verifyUrlBtn = document.getElementById('verify-aem-url');
+ const saveConfigBtn = document.getElementById('save-aem-config');
+
+ if (repositoryIdInput) {
+ repositoryIdInput.addEventListener('input', (e) => {
+ state.aemAssetsConfig.repositoryId = e.target.value.trim();
+ });
+ }
+
+ if (prodOriginInput) {
+ prodOriginInput.addEventListener('input', (e) => {
+ state.aemAssetsConfig.prodOrigin = e.target.value.trim();
+ });
+ }
+
+ if (verifyUrlBtn) {
+ verifyUrlBtn.addEventListener('click', () => app.handleVerifyAemUrl());
+ }
+
+ const checkboxes = [
+ { id: 'aem-image-type', key: 'imageType' },
+ { id: 'aem-renditions-select', key: 'renditionsSelect' },
+ { id: 'aem-dm-delivery', key: 'dmDelivery' },
+ { id: 'aem-smartcrop-select', key: 'smartCropSelect' },
+ ];
+
+ checkboxes.forEach(({ id, key }) => {
+ const checkbox = document.getElementById(id);
+ if (checkbox) {
+ checkbox.addEventListener('change', (e) => {
+ state.aemAssetsConfig[key] = e.target.checked;
+ });
+ }
+ });
+
+ if (saveConfigBtn) {
+ saveConfigBtn.addEventListener('click', () => app.handleSaveAemConfig());
+ }
+}
+
+export async function handleSaveAemConfig(state, render, daApi) {
+ const { repositoryId } = state.aemAssetsConfig;
+
+ if (!repositoryId) {
+ state.errors.aemAssets = 'Repository ID is required';
+ render();
+ return;
+ }
+
+ if (!repositoryId.startsWith('author-') && !repositoryId.startsWith('delivery-')) {
+ state.errors.aemAssets = 'Repository ID must start with "author-" or "delivery-"';
+ render();
+ return;
+ }
+
+ state.errors.aemAssets = '';
+ render();
+
+ try {
+ const result = await daApi.updateAemAssetsConfig(
+ state.org,
+ state.site,
+ state.aemAssetsConfig,
+ );
+
+ if (result.success) {
+ state.errors.aemAssets = '✓ Configuration saved successfully!';
+ } else {
+ state.errors.aemAssets = `Error saving configuration: ${result.error}`;
+ }
+ render();
+ } catch (error) {
+ state.errors.aemAssets = `Error saving configuration: ${error.message}`;
+ render();
+ }
+}
+
+export async function handleVerifyAemUrl(state, render) {
+ if (!state.aemAssetsConfig.prodOrigin) {
+ return;
+ }
+
+ state.validatingAemUrl = true;
+ state.errors.aemAssets = '';
+ render();
+
+ try {
+ await fetch(state.aemAssetsConfig.prodOrigin, {
+ method: 'HEAD',
+ mode: 'no-cors',
+ });
+ state.validatingAemUrl = false;
+ state.errors.aemAssets = '✓ URL verified successfully!';
+ render();
+ } catch (error) {
+ state.validatingAemUrl = false;
+ state.errors.aemAssets = `Unable to verify URL: ${error.message}`;
+ render();
+ }
+}
diff --git a/tools/admin/app/handlers/blocks-handlers.js b/tools/admin/app/handlers/blocks-handlers.js
new file mode 100644
index 0000000..e96d3a4
--- /dev/null
+++ b/tools/admin/app/handlers/blocks-handlers.js
@@ -0,0 +1,244 @@
+import * as githubOps from '../../operations/github.js';
+import * as libraryOps from '../../operations/library.js';
+import * as daApi from '../../utils/da-api.js';
+import TokenStorage from '../../utils/token-storage.js';
+
+// discoverBlocks must be defined before validateRepository which calls it
+export async function discoverBlocks(app, state) {
+ state.discovering = true;
+ app.render();
+ try {
+ const blocks = await githubOps.discoverBlocks(state.org, state.repo, state.githubToken);
+
+ const existingBlocksJSON = await daApi.fetchBlocksJSON(state.org, state.site);
+
+ const existingBlockNames = new Set(
+ existingBlocksJSON?.data?.data?.map((b) => {
+ const pathParts = b.path.split('/');
+ return pathParts[pathParts.length - 1];
+ }) || [],
+ );
+
+ state.blocks = blocks.map((block) => ({
+ ...block,
+ isNew: !existingBlockNames.has(block.name),
+ }));
+
+ state.blocksDiscovered = true;
+ state.discovering = false;
+
+ state.selectedBlocks = new Set(blocks.map((b) => b.name));
+
+ const libraryCheck = await libraryOps.checkLibraryExists(state.org, state.site);
+ state.libraryExists = libraryCheck.exists;
+
+ app.render();
+ } catch (error) {
+ state.errors.github = `Block discovery failed: ${error.message}`;
+ state.discovering = false;
+ app.render();
+ }
+}
+
+export async function handleGitHubUrlChange(state, render, url) {
+ state.githubUrl = url.trim();
+
+ const parsed = githubOps.parseGitHubURL(state.githubUrl);
+ if (parsed && parsed.org && parsed.repo) {
+ state.org = parsed.org;
+ state.repo = parsed.repo;
+ state.site = parsed.repo;
+ } else {
+ state.org = '';
+ state.repo = '';
+ }
+
+ state.repositoryValidated = false;
+ state.needsToken = false;
+ state.errors.github = '';
+ render();
+}
+
+export async function validateRepository(app, state) {
+ state.validating = true;
+ app.render();
+ try {
+ const result = await githubOps.validateRepository(state.org, state.repo, state.githubToken);
+
+ if (!result.valid) {
+ if (result.error === 'rate_limit') {
+ state.needsToken = true;
+ state.errors.github = `GitHub API rate limit exceeded (resets at ${result.resetTime}). Please add a GitHub token to continue, or wait and try again.`;
+ state.validating = false;
+ app.render();
+ return;
+ }
+
+ if (result.error === 'not_found') {
+ state.errors.github = 'Repository not found. Please check the URL and try again.';
+ state.validating = false;
+ app.render();
+ return;
+ }
+
+ if (result.error === 'private' && result.needsToken) {
+ state.needsToken = true;
+ state.errors.github = 'Unable to access repository. If this is a private repository, please enter a GitHub token below.';
+ state.validating = false;
+ app.render();
+ return;
+ }
+
+ state.errors.github = result.error === 'private' ? 'Unable to access repository with provided token.' : result.error;
+ state.validating = false;
+ app.render();
+ return;
+ }
+
+ state.repositoryValidated = true;
+ state.needsToken = false;
+
+ state.validating = false;
+ app.render();
+ await discoverBlocks(app, state);
+ } catch (error) {
+ let errorMsg = error.message;
+ if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
+ errorMsg = 'Please enter a valid GitHub repository URL.';
+ } else if (errorMsg.includes('Failed to fetch') || errorMsg.includes('NetworkError')) {
+ errorMsg = 'Network error. Please check your connection and try again.';
+ }
+
+ state.message = errorMsg;
+ state.messageType = 'error';
+ state.validating = false;
+ app.render();
+ }
+}
+
+export async function handleValidateWithToken(app, state) {
+ const tokenInput = document.getElementById('github-token');
+ const saveCheckbox = document.getElementById('save-token');
+ const token = tokenInput?.value.trim();
+
+ if (!token) {
+ state.errors.github = 'Please enter a GitHub token';
+ app.render();
+ return;
+ }
+
+ if (saveCheckbox?.checked) {
+ TokenStorage.set(token);
+ }
+
+ state.githubToken = token;
+ await validateRepository(app, state);
+}
+
+export function handleClearToken(state, render) {
+ TokenStorage.clear();
+ state.githubToken = null;
+ state.message = 'Saved token cleared';
+ state.messageType = 'success';
+ render();
+}
+
+export async function handleLoadExistingBlocks(state, render) {
+ if (!state.org || !state.site) {
+ state.errors.site = 'Please enter both organization and site name';
+ render();
+ return;
+ }
+
+ state.discovering = true;
+ render();
+
+ try {
+ const blocks = await libraryOps.fetchExistingBlocks(state.org, state.site);
+
+ if (blocks.length === 0) {
+ state.errors.site = 'No library found at this location. Please run "Library Setup" first to create the library.';
+ state.discovering = false;
+ render();
+ return;
+ }
+
+ state.blocks = blocks.map((block) => ({
+ ...block,
+ isNew: false,
+ }));
+ state.blocksDiscovered = true;
+ state.discovering = false;
+ state.selectedBlocks = new Set(blocks.map((b) => b.name));
+ state.errors = {
+ github: '', site: '', blocks: '', templates: '', icons: '', placeholders: '', pages: '',
+ };
+ render();
+ } catch (error) {
+ state.errors.site = `Failed to load blocks: ${error.message}`;
+ state.discovering = false;
+ render();
+ }
+}
+
+export function toggleAllBlocks(state, render) {
+ if (state.selectedBlocks.size === state.blocks.length) {
+ state.selectedBlocks.clear();
+ } else {
+ state.blocks.forEach((block) => state.selectedBlocks.add(block.name));
+ }
+ render();
+}
+
+export function selectNewBlocksOnly(state, render) {
+ state.selectedBlocks.clear();
+ state.blocks
+ .filter((block) => block.isNew)
+ .forEach((block) => state.selectedBlocks.add(block.name));
+ render();
+}
+
+export function attachBlocksListeners(app, state) {
+ const validateRepoBtn = document.getElementById('validate-repository');
+ if (validateRepoBtn) {
+ validateRepoBtn.addEventListener('click', () => validateRepository(app, state));
+ }
+
+ const loadExistingBlocksBtn = document.getElementById('load-existing-blocks');
+ if (loadExistingBlocksBtn) {
+ loadExistingBlocksBtn.addEventListener('click', () => handleLoadExistingBlocks(state, () => app.render()));
+ }
+
+ const validateWithTokenBtn = document.getElementById('validate-with-token');
+ if (validateWithTokenBtn) {
+ validateWithTokenBtn.addEventListener('click', () => handleValidateWithToken(app, state));
+ }
+
+ const clearTokenBtn = document.getElementById('clear-token');
+ if (clearTokenBtn) {
+ clearTokenBtn.addEventListener('click', () => handleClearToken(state, () => app.render()));
+ }
+
+ document.querySelectorAll('[data-block-name]').forEach((checkbox) => {
+ checkbox.addEventListener('change', (e) => {
+ const { target } = e;
+ const { blockName } = target.dataset;
+ if (target.checked) {
+ state.selectedBlocks.add(blockName);
+ } else {
+ state.selectedBlocks.delete(blockName);
+ }
+ app.render();
+ });
+ });
+
+ const toggleAllBtn = document.getElementById('toggle-all-blocks');
+ if (toggleAllBtn) {
+ toggleAllBtn.addEventListener('click', () => toggleAllBlocks(state, () => app.render()));
+ }
+
+ const selectNewOnlyBtn = document.getElementById('select-new-only');
+ if (selectNewOnlyBtn) {
+ selectNewOnlyBtn.addEventListener('click', () => selectNewBlocksOnly(state, () => app.render()));
+ }
+}
diff --git a/tools/admin/app/handlers/library-item-handler-factory.js b/tools/admin/app/handlers/library-item-handler-factory.js
new file mode 100644
index 0000000..a256480
--- /dev/null
+++ b/tools/admin/app/handlers/library-item-handler-factory.js
@@ -0,0 +1,233 @@
+/**
+ * Factory for creating generic library item handlers (templates, icons, placeholders)
+ * Consolidates duplicate handler logic across multiple item types
+ */
+
+function capitalize(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+/**
+ * Creates a generic handler for library items
+ * @param {string} itemType - Item type (e.g., 'template', 'icon', 'placeholder')
+ * @param {Object} config - Configuration for the handler
+ * @param {string[]} config.formFields - Field names in the form
+ * @param {string} [config.confirmMessage] - Confirmation message for removal
+ * @param {boolean} [config.hasPagePicker] - Whether to show page picker button
+ * @returns {Object} Handler object with all methods
+ */
+export function createLibraryItemHandler(itemType, config) {
+ const {
+ formFields = ['name', 'path'],
+ confirmMessage = `Remove this ${itemType} from the library?`,
+ hasPagePicker = false,
+ } = config;
+
+ const capitalizedType = capitalize(itemType);
+ const pluralType = `${itemType}s`;
+ const capitalizedPlural = capitalize(pluralType);
+
+ return {
+ attachListeners(app, state) {
+ const searchInput = document.getElementById(`${itemType}-search`);
+ if (searchInput) {
+ searchInput.addEventListener('input', (e) => {
+ state[`${itemType}SearchQuery`] = e.target.value;
+ app.render();
+ });
+ }
+
+ document.querySelectorAll(`.edit-item-btn[data-type="${itemType}"]`).forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ const index = parseInt(e.target.dataset.index, 10);
+ app[`handleEditExisting${capitalizedType}`](index);
+ });
+ });
+
+ document.querySelectorAll(`.remove-item-btn[data-type="${itemType}"]`).forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ const index = parseInt(e.target.dataset.index, 10);
+ app[`handleRemoveExisting${capitalizedType}`](index);
+ });
+ });
+
+ const inputs = {};
+ const addBtn = document.getElementById(`add-${itemType}`);
+
+ formFields.forEach((field) => {
+ const inputId = `${itemType}-${field}`;
+ inputs[field] = document.getElementById(inputId);
+ });
+
+ if (Object.values(inputs).every((input) => input) && addBtn) {
+ const updateValidation = () => {
+ const allValid = formFields.every((field) => {
+ if (field === 'path') {
+ return state[`${itemType}Form`][field].length > 0;
+ }
+ return inputs[field].value.trim().length > 0;
+ });
+
+ addBtn.disabled = !allValid;
+
+ const firstField = formFields[0];
+ if (inputs[firstField]) {
+ if (inputs[firstField].value.trim().length === 0) {
+ inputs[firstField].classList.add('validation-required');
+ } else {
+ inputs[firstField].classList.remove('validation-required');
+ }
+ }
+ };
+
+ formFields.forEach((field) => {
+ if (inputs[field]) {
+ inputs[field].addEventListener('input', (e) => {
+ state[`${itemType}Form`][field] = e.target.value.trim();
+ updateValidation();
+ });
+
+ inputs[field].addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !addBtn.disabled) {
+ e.preventDefault();
+ app[`handleAdd${capitalizedType}`]();
+ }
+ });
+ }
+ });
+
+ updateValidation();
+ }
+
+ if (hasPagePicker) {
+ const selectPageBtn = document.getElementById(`select-${itemType}-page`);
+ if (selectPageBtn) {
+ selectPageBtn.addEventListener('click', () => app.openPagePicker(state.site, pluralType));
+ }
+ }
+
+ if (addBtn) {
+ addBtn.addEventListener('click', () => app[`handleAdd${capitalizedType}`]());
+ }
+
+ const cancelEditBtn = document.getElementById(`cancel-edit-${itemType}`);
+ if (cancelEditBtn) {
+ cancelEditBtn.addEventListener('click', () => app[`handleCancelEdit${capitalizedType}`]());
+ }
+
+ document.querySelectorAll(`.remove-${itemType}-btn`).forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ const index = parseInt(e.target.dataset.index, 10);
+ app[`handleRemove${capitalizedType}`](index);
+ });
+ });
+ },
+
+ handleAdd(state, render) {
+ const formKey = `${itemType}Form`;
+ const form = state[formKey];
+
+ const isValid = formFields.every((field) => form[field] && form[field].length > 0);
+ if (!isValid) {
+ return;
+ }
+
+ const editingKey = `editing${capitalizedType}Index`;
+ const existingKey = `existing${capitalizedPlural}`;
+ const selectedKey = `selected${capitalizedPlural}`;
+
+ if (state[editingKey] >= 0) {
+ const updatedItem = {};
+ formFields.forEach((field) => {
+ updatedItem[field] = form[field];
+ });
+
+ state[existingKey][state[editingKey]] = updatedItem;
+ state[selectedKey].push(updatedItem);
+ state[editingKey] = -1;
+ } else {
+ const newItem = {};
+ formFields.forEach((field) => {
+ newItem[field] = form[field];
+ });
+ state[selectedKey].push(newItem);
+ }
+
+ const emptyForm = {};
+ formFields.forEach((field) => {
+ emptyForm[field] = '';
+ });
+ state[formKey] = emptyForm;
+ state.errors[pluralType] = '';
+ render();
+ },
+
+ handleRemove(state, render, index) {
+ const selectedKey = `selected${capitalizedPlural}`;
+ state[selectedKey].splice(index, 1);
+ render();
+ },
+
+ handleEditExisting(state, render, index) {
+ const existingKey = `existing${capitalizedPlural}`;
+ const formKey = `${itemType}Form`;
+ const editingKey = `editing${capitalizedType}Index`;
+
+ const item = state[existingKey][index];
+ const form = {};
+ formFields.forEach((field) => {
+ form[field] = item[field];
+ });
+
+ state[formKey] = form;
+ state[editingKey] = index;
+ render();
+ },
+
+ handleRemoveExisting(state, render, index) {
+ // TODO: Replace window.confirm with custom modal for better UX
+ // eslint-disable-next-line no-alert
+ if (window.confirm(confirmMessage)) {
+ const existingKey = `existing${capitalizedPlural}`;
+ const selectedKey = `selected${capitalizedPlural}`;
+
+ const item = state[existingKey][index];
+ state[existingKey].splice(index, 1);
+ state[selectedKey].push({
+ ...item,
+ _removed: true,
+ });
+ render();
+ }
+ },
+
+ handleCancelEdit(state, render) {
+ const formKey = `${itemType}Form`;
+ const editingKey = `editing${capitalizedType}Index`;
+
+ const emptyForm = {};
+ formFields.forEach((field) => {
+ emptyForm[field] = '';
+ });
+
+ state[formKey] = emptyForm;
+ state[editingKey] = -1;
+ render();
+ },
+ };
+}
+
+export const templatesHandler = createLibraryItemHandler('template', {
+ formFields: ['name', 'path'],
+ hasPagePicker: true,
+});
+
+export const iconsHandler = createLibraryItemHandler('icon', {
+ formFields: ['name', 'path'],
+ hasPagePicker: true,
+});
+
+export const placeholdersHandler = createLibraryItemHandler('placeholder', {
+ formFields: ['key', 'value'],
+ hasPagePicker: false,
+});
diff --git a/tools/admin/app/handlers/page-picker-handlers.js b/tools/admin/app/handlers/page-picker-handlers.js
new file mode 100644
index 0000000..6bd35ba
--- /dev/null
+++ b/tools/admin/app/handlers/page-picker-handlers.js
@@ -0,0 +1,294 @@
+import * as pagesOps from '../../operations/pages.js';
+import * as templates from '../templates.js';
+
+// Circular dependency between renderPagePickerModalInternal and attachPagePickerListeners
+// is intentional: modal needs to attach listeners after rendering, and listeners re-render
+/* eslint-disable no-use-before-define */
+
+export async function validateSite(org, site, token) {
+ try {
+ const response = await fetch(
+ `https://admin.da.live/list/${org}/${site}/`,
+ {
+ method: 'GET',
+ headers: { Authorization: `Bearer ${token}` },
+ },
+ );
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+}
+
+export function closePagePicker(state) {
+ const modalContainer = document.getElementById('page-picker-modal');
+ if (modalContainer) {
+ modalContainer.remove();
+ }
+ state.showPagePicker = false;
+ state.pageSearchQuery = '';
+}
+
+export function confirmPageSelection(app, state) {
+ if (state.pagePickerMode === 'templates') {
+ const selectedPages = Array.from(state.pageSelections[state.currentSite] || []);
+ if (selectedPages.length > 0) {
+ [state.templateForm.path] = selectedPages;
+ }
+ state.pageSelections = {};
+ } else if (state.pagePickerMode === 'icons') {
+ const selectedPages = Array.from(state.pageSelections[state.currentSite] || []);
+ if (selectedPages.length > 0) {
+ [state.iconForm.path] = selectedPages;
+ }
+ state.pageSelections = {};
+ }
+ closePagePicker(state);
+ state.pagePickerMode = '';
+ app.render();
+}
+
+function renderPagePickerModalInternal(app, state) {
+ const modal = templates.pagePickerModalTemplate({
+ site: state.currentSite,
+ items: state.allPages,
+ selectedPages: state.pageSelections[state.currentSite] || new Set(),
+ loading: state.loadingPages,
+ mode: state.pagePickerMode,
+ });
+
+ let modalContainer = document.getElementById('page-picker-modal');
+ if (!modalContainer) {
+ modalContainer = document.createElement('div');
+ modalContainer.id = 'page-picker-modal';
+ document.body.appendChild(modalContainer);
+ }
+
+ modalContainer.innerHTML = modal;
+ attachPagePickerListeners(app, state);
+}
+
+export function attachPagePickerListeners(app, state) {
+ const overlay = document.querySelector('.modal-overlay');
+ if (overlay) {
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) {
+ closePagePicker(state);
+ }
+ });
+ }
+
+ const cancelBtn = document.querySelector('.modal-cancel');
+ if (cancelBtn) {
+ cancelBtn.addEventListener('click', () => closePagePicker(state));
+ }
+
+ const confirmBtn = document.querySelector('.modal-confirm');
+ if (confirmBtn) {
+ confirmBtn.addEventListener('click', () => confirmPageSelection(app, state));
+ }
+
+ const searchInput = document.getElementById('page-search');
+ if (searchInput) {
+ searchInput.addEventListener('input', (e) => {
+ state.pageSearchQuery = e.target.value;
+ renderPagePickerModalInternal(app, state);
+ });
+ }
+
+ document.querySelectorAll('.folder-toggle').forEach((folderBtn) => {
+ folderBtn.addEventListener('click', async (e) => {
+ const button = e.currentTarget;
+ const folderItem = button.closest('.folder-item');
+ const contents = folderItem.querySelector('.folder-contents');
+ const arrow = button.querySelector('.toggle-arrow');
+ const icon = button.querySelector('.folder-icon');
+ const { folderPath } = button.dataset;
+ const isLoaded = contents.dataset.loaded === 'true';
+
+ if (contents.classList.contains('hidden')) {
+ contents.classList.remove('hidden');
+ arrow.textContent = '▼';
+ icon.textContent = '📂';
+
+ if (!isLoaded) {
+ try {
+ const allItems = await pagesOps.loadFolderContents(
+ state.org,
+ state.currentSite,
+ folderPath,
+ );
+
+ let targetExt = null;
+ if (state.pagePickerMode === 'templates') {
+ targetExt = 'html';
+ } else if (state.pagePickerMode === 'icons') {
+ targetExt = 'svg';
+ }
+
+ const filteredItems = targetExt
+ ? allItems.filter((item) => !item.ext || item.ext === targetExt)
+ : allItems;
+
+ let childHTML;
+
+ if (allItems.length === 0) {
+ childHTML = '
Empty folder
';
+ } else if (filteredItems.length === 0) {
+ let fileTypeMsg = 'No matching files';
+ if (targetExt === 'html') {
+ fileTypeMsg = 'No HTML files';
+ } else if (targetExt === 'svg') {
+ fileTypeMsg = 'No SVG files';
+ }
+ childHTML = `${fileTypeMsg}
`;
+ } else {
+ const isSingleSelect = state.pagePickerMode === 'templates'
+ || state.pagePickerMode === 'icons';
+ const inputType = isSingleSelect ? 'radio' : 'checkbox';
+ const inputName = isSingleSelect ? 'selected-item' : '';
+
+ childHTML = filteredItems
+ .sort((a, b) => {
+ if (!a.ext && b.ext) return -1;
+ if (a.ext && !b.ext) return 1;
+ return a.name.localeCompare(b.name);
+ })
+ .map((item) => {
+ if (item.ext) {
+ const siteSelections = state.pageSelections[state.currentSite] || new Set();
+ const isSelected = siteSelections.has(item.path);
+ const displayName = item.name.replace(`.${item.ext}`, '');
+ let fileIcon;
+
+ if (item.ext === 'svg') {
+ const iconUrl = `https://content.da.live${item.path}`;
+ fileIcon = ` `;
+ } else {
+ fileIcon = '📄';
+ }
+
+ return `
+
+
+
+ ${fileIcon}
+ ${displayName}
+
+
+ `;
+ }
+ return `
+
+
+ 📁
+ ${item.name}
+ ▶
+
+
+
+ `;
+ })
+ .join('');
+ }
+
+ contents.innerHTML = childHTML;
+ contents.dataset.loaded = 'true';
+
+ attachPagePickerListeners(app, state);
+ } catch (error) {
+ contents.innerHTML = 'Failed to load
';
+ }
+ }
+ } else {
+ contents.classList.add('hidden');
+ arrow.textContent = '▶';
+ icon.textContent = '📁';
+ }
+ });
+ });
+
+ document.querySelectorAll('.page-checkbox input[type="checkbox"], .page-checkbox input[type="radio"]').forEach((input) => {
+ input.addEventListener('change', (e) => {
+ const { path } = e.target.dataset;
+ if (!state.pageSelections[state.currentSite]) {
+ state.pageSelections[state.currentSite] = new Set();
+ }
+
+ if (e.target.type === 'radio') {
+ state.pageSelections[state.currentSite].clear();
+ if (e.target.checked) {
+ state.pageSelections[state.currentSite].add(path);
+ }
+ } else if (e.target.checked) {
+ state.pageSelections[state.currentSite].add(path);
+ } else {
+ state.pageSelections[state.currentSite].delete(path);
+ }
+
+ const confirmButton = document.querySelector('.modal-confirm');
+ if (confirmButton) {
+ const count = state.pageSelections[state.currentSite].size;
+ confirmButton.textContent = `Confirm (${count} selected)`;
+ }
+ });
+ });
+}
+
+export function removePage(state, render, site, path) {
+ if (state.pageSelections[site]) {
+ state.pageSelections[site].delete(path);
+ }
+ render();
+}
+
+export function renderPagePickerModal(app, state) {
+ renderPagePickerModalInternal(app, state);
+}
+
+export async function openPagePicker(app, state, site, mode = 'pages') {
+ if (!state.daToken) {
+ state.errors[mode] = 'DA.live authentication required. This tool must be run from within DA.live.';
+ app.render();
+ return;
+ }
+
+ const siteValid = await validateSite(state.org, site, state.daToken);
+ if (!siteValid) {
+ state.errors[mode] = `Site "${state.org}/${site}" not found in DA.live. Please verify the site name.`;
+ app.render();
+ return;
+ }
+
+ state.pagePickerMode = mode;
+ state.currentSite = site;
+ state.loadingPages = true;
+ state.showPagePicker = true;
+ renderPagePickerModalInternal(app, state);
+
+ try {
+ const pages = await pagesOps.fetchSitePages(state.org, site);
+ let filteredPages = pages;
+
+ if (mode === 'templates') {
+ filteredPages = pages.filter((page) => !page.ext || page.ext === 'html');
+ } else if (mode === 'icons') {
+ filteredPages = pages.filter((page) => !page.ext || page.ext === 'svg');
+ }
+
+ state.allPages = filteredPages;
+ state.loadingPages = false;
+ renderPagePickerModalInternal(app, state);
+ } catch (error) {
+ const errorMsg = error.message.includes('401')
+ ? 'Authentication failed. Please ensure you are logged in to DA.live.'
+ : `Failed to load pages: ${error.message}`;
+
+ state.errors[mode] = errorMsg;
+ state.loadingPages = false;
+ state.showPagePicker = false;
+ app.render();
+ }
+}
diff --git a/tools/admin/app/handlers/translation-handlers.js b/tools/admin/app/handlers/translation-handlers.js
new file mode 100644
index 0000000..2655be1
--- /dev/null
+++ b/tools/admin/app/handlers/translation-handlers.js
@@ -0,0 +1,51 @@
+export function attachTranslationListeners(app, state) {
+ const translateBehaviorSelect = document.getElementById('translate-behavior');
+ const translateStagingSelect = document.getElementById('translate-staging');
+ const rolloutBehaviorSelect = document.getElementById('rollout-behavior');
+ const saveConfigBtn = document.getElementById('save-translation-config');
+
+ if (translateBehaviorSelect) {
+ translateBehaviorSelect.addEventListener('change', (e) => {
+ state.translationConfig.translateBehavior = e.target.value;
+ });
+ }
+
+ if (translateStagingSelect) {
+ translateStagingSelect.addEventListener('change', (e) => {
+ state.translationConfig.translateStaging = e.target.value;
+ });
+ }
+
+ if (rolloutBehaviorSelect) {
+ rolloutBehaviorSelect.addEventListener('change', (e) => {
+ state.translationConfig.rolloutBehavior = e.target.value;
+ });
+ }
+
+ if (saveConfigBtn) {
+ saveConfigBtn.addEventListener('click', () => app.handleSaveTranslationConfig());
+ }
+}
+
+export async function handleSaveTranslationConfig(state, render, daApi) {
+ state.errors.translation = '';
+ render();
+
+ try {
+ const result = await daApi.updateTranslationConfig(
+ state.org,
+ state.site,
+ state.translationConfig,
+ );
+
+ if (result.success) {
+ state.errors.translation = '✓ Configuration saved successfully!';
+ } else {
+ state.errors.translation = `Error saving configuration: ${result.error}`;
+ }
+ render();
+ } catch (error) {
+ state.errors.translation = `Error saving configuration: ${error.message}`;
+ render();
+ }
+}
diff --git a/tools/admin/app/handlers/universal-editor-handlers.js b/tools/admin/app/handlers/universal-editor-handlers.js
new file mode 100644
index 0000000..2310eb2
--- /dev/null
+++ b/tools/admin/app/handlers/universal-editor-handlers.js
@@ -0,0 +1,43 @@
+export function attachUniversalEditorListeners(app, state) {
+ const editorPathInput = document.getElementById('ue-editor-path');
+ const saveConfigBtn = document.getElementById('save-ue-config');
+
+ if (editorPathInput) {
+ editorPathInput.addEventListener('input', (e) => {
+ state.universalEditorConfig.editorPath = e.target.value.trim();
+ });
+ }
+
+ if (saveConfigBtn) {
+ saveConfigBtn.addEventListener('click', () => app.handleSaveUeConfig());
+ }
+}
+
+export async function handleSaveUeConfig(state, render, daApi) {
+ if (!state.universalEditorConfig.editorPath) {
+ state.errors.universalEditor = 'Editor Path is required';
+ render();
+ return;
+ }
+
+ state.errors.universalEditor = '';
+ render();
+
+ try {
+ const result = await daApi.updateUniversalEditorConfig(
+ state.org,
+ state.site,
+ state.universalEditorConfig,
+ );
+
+ if (result.success) {
+ state.errors.universalEditor = '✓ Configuration saved successfully!';
+ } else {
+ state.errors.universalEditor = `Error saving configuration: ${result.error}`;
+ }
+ render();
+ } catch (error) {
+ state.errors.universalEditor = `Error saving configuration: ${error.message}`;
+ render();
+ }
+}
diff --git a/tools/admin/app/main.js b/tools/admin/app/main.js
new file mode 100644
index 0000000..03aacc0
--- /dev/null
+++ b/tools/admin/app/main.js
@@ -0,0 +1,1060 @@
+/* eslint-disable import/no-absolute-path */
+
+/* eslint-disable import/no-unresolved */
+import DA_SDK from 'https://da.live/nx/utils/sdk.js';
+
+import state, { resetModeState, clearErrors } from './state.js';
+import * as templates from './templates.js';
+import * as githubOps from '../operations/github.js';
+import * as libraryOps from '../operations/library.js';
+import * as daApi from '../utils/da-api.js';
+import TokenStorage from '../utils/token-storage.js';
+import GitHubAPI from '../utils/github-api.js';
+import loadCSS from '../utils/css-loader.js';
+import { templatesHandler, iconsHandler, placeholdersHandler } from './handlers/library-item-handler-factory.js';
+import * as pagePickerHandlers from './handlers/page-picker-handlers.js';
+import * as blocksHandlers from './handlers/blocks-handlers.js';
+import * as aemAssetsHandlers from './handlers/aem-assets-handlers.js';
+import * as translationHandlers from './handlers/translation-handlers.js';
+import * as universalEditorHandlers from './handlers/universal-editor-handlers.js';
+import { initRouter, getCurrentRoute } from './router.js';
+import * as libraryItemsManager from '../operations/library-items-manager.js';
+
+const app = {
+ async init() {
+ const container = document.getElementById('app-container');
+ if (!container) {
+ throw new Error('App container not found');
+ }
+
+ try {
+ const { context, token } = await DA_SDK;
+ state.daToken = token;
+
+ if (context?.org) {
+ state.org = context.org;
+ }
+
+ if (context?.repo) {
+ state.site = context.repo;
+ }
+ } catch (error) {
+ const urlMatch = window.location.pathname.match(/^\/app\/([^/]+)\/([^/]+)/);
+ if (urlMatch) {
+ const [, org, site] = urlMatch;
+ state.org = org;
+ state.site = site;
+ }
+ }
+
+ if (TokenStorage.exists()) {
+ state.githubToken = TokenStorage.get();
+ }
+
+ this.container = container;
+ await this.loadInitialCSS();
+
+ initRouter({
+ blocks: () => this.renderBlocksView(),
+ templates: () => this.renderTemplatesView(),
+ icons: () => this.renderIconsView(),
+ placeholders: () => this.renderPlaceholdersView(),
+ 'aem-assets': () => this.renderAemAssetsView(),
+ translation: () => this.renderTranslationView(),
+ 'universal-editor': () => this.renderUniversalEditorView(),
+ });
+
+ this.attachEventListeners();
+
+ if (state.mode === 'refresh' && state.org && state.site) {
+ await this.handleLoadExistingBlocks();
+ }
+ },
+
+ async loadInitialCSS() {
+ await Promise.all([
+ loadCSS('admin.css'),
+ loadCSS('progress.css'),
+ loadCSS('error-modal.css'),
+ ]);
+ },
+
+ async renderBlocksView() {
+ const cssToLoad = [];
+
+ if (state.mode === 'setup' && !state.repositoryValidated) {
+ cssToLoad.push(loadCSS('github-section.css'));
+ }
+
+ if (state.blocksDiscovered || state.mode === 'refresh') {
+ cssToLoad.push(loadCSS('blocks-section.css'));
+ }
+
+ if (state.selectedBlocks.size > 0) {
+ cssToLoad.push(loadCSS('page-picker.css'));
+ }
+
+ if (cssToLoad.length > 0) {
+ await Promise.all(cssToLoad);
+ }
+
+ const sections = [];
+
+ sections.push(templates.sectionHeaderTemplate({
+ title: 'Blocks',
+ description: 'Generate block documentation for your DA.live library from GitHub repositories. Choose between initial setup mode or update existing blocks with new content examples.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-library',
+ }));
+
+ sections.push(templates.modeToggleTemplate({
+ currentMode: state.mode,
+ }));
+
+ if (state.mode === 'setup') {
+ sections.push(templates.githubSectionTemplate({
+ isValidated: state.repositoryValidated,
+ validating: state.validating,
+ githubUrl: state.githubUrl,
+ message: state.errors.github ? templates.messageTemplate(state.errors.github, 'error') : '',
+ }));
+
+ if (state.needsToken && !state.repositoryValidated) {
+ sections.push(templates.tokenInputTemplate({
+ hasSavedToken: TokenStorage.exists(),
+ }));
+ }
+ }
+
+ if ((state.mode === 'setup' && state.repositoryValidated) || state.mode === 'refresh') {
+ sections.push(templates.siteSectionTemplate({
+ org: state.org,
+ site: state.site,
+ message: state.errors.site ? templates.messageTemplate(state.errors.site, 'error') : '',
+ mode: state.mode,
+ }));
+ }
+
+ if (state.blocksDiscovered) {
+ sections.push(templates.blocksListTemplate({
+ blocks: state.blocks,
+ selectedBlocks: state.selectedBlocks,
+ message: state.errors.blocks ? templates.messageTemplate(state.errors.blocks, 'error') : '',
+ }));
+
+ if (state.selectedBlocks.size > 0) {
+ sections.push(templates.pagesSelectionTemplate({
+ allSites: this.getAllSites(),
+ pageSelections: state.pageSelections,
+ message: state.errors.pages ? templates.messageTemplate(state.errors.pages, 'error') : '',
+ daToken: state.daToken,
+ org: state.org,
+ mode: state.mode,
+ }));
+ }
+ }
+
+ const hasContent = state.selectedBlocks.size > 0;
+
+ if (hasContent && !state.processStatus?.completed) {
+ sections.push(templates.startButtonTemplate({
+ mode: state.mode,
+ disabled: state.processing,
+ processing: state.processing,
+ }));
+ }
+
+ if (hasContent && (state.processing || state.processStatus?.completed)) {
+ sections.push(state.processing
+ ? templates.processingTemplate({ processStatus: state.processStatus })
+ : templates.finalStatusTemplate({
+ processStatus: state.processStatus,
+ org: state.org,
+ repo: state.repo || state.site,
+ }));
+ } else if (state.selectedBlocks.size > 0) {
+ sections.push(templates.initialStatusTemplate({
+ org: state.org,
+ repo: state.repo,
+ blocksCount: state.blocks.length,
+ mode: state.mode,
+ libraryExists: state.libraryExists,
+ }));
+ }
+
+ const content = sections.join('');
+ const errorModal = templates.errorModalTemplate(
+ state.processStatus.errors.messages,
+ );
+ this.container.innerHTML = templates.layoutTemplate(
+ getCurrentRoute(),
+ content,
+ ) + errorModal;
+ this.attachEventListeners();
+ },
+
+ async renderLibraryItemView(itemType, config) {
+ const {
+ title,
+ description,
+ docsUrl,
+ templateFunction,
+ } = config;
+
+ await loadCSS('library-items-section.css');
+ await loadCSS('page-picker.css');
+
+ const capitalizedType = itemType.charAt(0).toUpperCase() + itemType.slice(1);
+ const pluralType = `${itemType}s`;
+ const capitalizedPlural = `${capitalizedType}s`;
+
+ const existingKey = `existing${capitalizedPlural}`;
+ const loadingKey = `loading${capitalizedPlural}`;
+ const searchKey = `${itemType}SearchQuery`;
+ const selectedKey = `selected${capitalizedPlural}`;
+ const formKey = `${itemType}Form`;
+ const editingKey = `editing${capitalizedType}Index`;
+
+ const shouldLoad = state[existingKey].length === 0
+ && !state[loadingKey]
+ && state.org
+ && state.site;
+
+ if (shouldLoad) {
+ state[loadingKey] = true;
+ state[existingKey] = await libraryItemsManager[`fetchExisting${capitalizedPlural}`](
+ state.org,
+ state.site,
+ );
+ state[loadingKey] = false;
+ }
+
+ const filteredExisting = libraryItemsManager.filterItems(
+ state[existingKey],
+ state[searchKey],
+ pluralType,
+ );
+
+ const sections = [];
+
+ sections.push(templates.sectionHeaderTemplate({
+ title,
+ description,
+ docsUrl,
+ }));
+
+ sections.push(templateFunction({
+ [`existing${capitalizedPlural}`]: filteredExisting,
+ [pluralType]: state[selectedKey],
+ [`${itemType}Form`]: state[formKey],
+ editingIndex: state[editingKey],
+ searchQuery: state[searchKey],
+ loading: state[loadingKey],
+ message: state.errors[pluralType] ? templates.messageTemplate(state.errors[pluralType], 'error') : '',
+ }));
+
+ const hasContent = state[selectedKey].length > 0;
+
+ if (hasContent && !state.processStatus?.completed) {
+ sections.push(templates.startButtonTemplate({
+ mode: 'setup',
+ disabled: state.processing,
+ processing: state.processing,
+ }));
+ }
+
+ if (hasContent && (state.processing || state.processStatus?.completed)) {
+ sections.push(state.processing
+ ? templates.processingTemplate({ processStatus: state.processStatus })
+ : templates.finalStatusTemplate({
+ processStatus: state.processStatus,
+ org: state.org,
+ repo: state.repo || state.site,
+ }));
+ }
+
+ const content = sections.join('');
+ const errorModal = templates.errorModalTemplate(
+ state.processStatus.errors.messages,
+ );
+ this.container.innerHTML = templates.layoutTemplate(
+ getCurrentRoute(),
+ content,
+ ) + errorModal;
+ this.attachEventListeners();
+ },
+
+ async renderTemplatesView() {
+ return this.renderLibraryItemView('template', {
+ title: 'Templates',
+ description: 'Create and manage page templates that authors can use to quickly build new pages with pre-configured layouts and content blocks.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-library',
+ templateFunction: templates.templatesSectionTemplate,
+ });
+ },
+
+ async renderIconsView() {
+ return this.renderLibraryItemView('icon', {
+ title: 'Icons',
+ description: 'Manage SVG icons that authors can insert into documents. Icons are referenced by name and can be used throughout your site for consistent visual elements.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-library',
+ templateFunction: templates.iconsSectionTemplate,
+ });
+ },
+
+ async renderPlaceholdersView() {
+ return this.renderLibraryItemView('placeholder', {
+ title: 'Placeholders',
+ description: 'Define reusable text placeholders (tokens) that authors can insert into documents. Perfect for commonly used text snippets, legal disclaimers, or dynamic content.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-library',
+ templateFunction: templates.placeholdersSectionTemplate,
+ });
+ },
+
+ async renderIntegrationView(integrationType, config) {
+ const {
+ title,
+ description,
+ docsUrl,
+ templateFunction,
+ configKey,
+ loadingKey,
+ errorKey,
+ fetchFunction,
+ shouldLoadCheck,
+ additionalProps = {},
+ } = config;
+
+ await loadCSS('integrations.css');
+
+ if (!state[loadingKey] && shouldLoadCheck()) {
+ state[loadingKey] = true;
+ state[configKey] = await fetchFunction(state.org, state.site);
+ state[loadingKey] = false;
+ }
+
+ const sections = [];
+
+ sections.push(templates.sectionHeaderTemplate({
+ title,
+ description,
+ docsUrl,
+ }));
+
+ const messageType = state.errors[errorKey]?.includes('successfully')
+ ? 'success'
+ : 'error';
+
+ sections.push(templateFunction({
+ config: state[configKey],
+ loading: state[loadingKey],
+ message: state.errors[errorKey]
+ ? templates.messageTemplate(state.errors[errorKey], messageType)
+ : '',
+ ...additionalProps,
+ }));
+
+ const content = sections.join('');
+ const errorModal = templates.errorModalTemplate(
+ state.processStatus.errors.messages,
+ );
+ this.container.innerHTML = templates.layoutTemplate(
+ getCurrentRoute(),
+ content,
+ ) + errorModal;
+ this.attachEventListeners();
+ },
+
+ async renderAemAssetsView() {
+ return this.renderIntegrationView('aemAssets', {
+ title: 'AEM Assets Integration',
+ description: 'Connect your AEM as a Cloud Service assets repository to enable authors to browse and insert assets directly from AEM into their documents.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-aem-assets',
+ templateFunction: templates.aemAssetsSectionTemplate,
+ configKey: 'aemAssetsConfig',
+ loadingKey: 'loadingAemConfig',
+ errorKey: 'aemAssets',
+ fetchFunction: daApi.fetchAemAssetsConfig,
+ shouldLoadCheck: () => state.org && state.site && state.aemAssetsConfig.repositoryId === '',
+ additionalProps: { validating: state.validatingAemUrl },
+ });
+ },
+
+ async renderTranslationView() {
+ return this.renderIntegrationView('translation', {
+ title: 'Translation Configuration',
+ description: 'Configure how translation services handle your content. Set up staging, behavior for handling existing content, and rollout strategies for localized sites.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-translation',
+ templateFunction: templates.translationSectionTemplate,
+ configKey: 'translationConfig',
+ loadingKey: 'loadingTranslationConfig',
+ errorKey: 'translation',
+ fetchFunction: daApi.fetchTranslationConfig,
+ shouldLoadCheck: () => state.org && state.site && state.translationConfig.translateBehavior === 'overwrite',
+ });
+ },
+
+ async renderUniversalEditorView() {
+ return this.renderIntegrationView('universalEditor', {
+ title: 'Universal Editor Setup',
+ description: 'Enable Universal Editor for WYSIWYG content authoring. Configure the editor path to allow authors to edit content directly in a visual interface with real-time preview.',
+ docsUrl: 'https://docs.da.live/administrators/guides/setup-universal-editor',
+ templateFunction: templates.universalEditorSectionTemplate,
+ configKey: 'universalEditorConfig',
+ loadingKey: 'loadingUeConfig',
+ errorKey: 'universalEditor',
+ fetchFunction: daApi.fetchUniversalEditorConfig,
+ shouldLoadCheck: () => state.org && state.site && state.universalEditorConfig.editorPath === '',
+ });
+ },
+
+ async render() {
+ const route = getCurrentRoute();
+ const viewMap = {
+ blocks: () => this.renderBlocksView(),
+ templates: () => this.renderTemplatesView(),
+ icons: () => this.renderIconsView(),
+ placeholders: () => this.renderPlaceholdersView(),
+ 'aem-assets': () => this.renderAemAssetsView(),
+ translation: () => this.renderTranslationView(),
+ 'universal-editor': () => this.renderUniversalEditorView(),
+ };
+
+ if (viewMap[route]) {
+ await viewMap[route]();
+ }
+ },
+
+ attachEventListeners() {
+ document.querySelectorAll('.mode-btn').forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ const newMode = e.target.dataset.mode;
+ if (newMode !== state.mode) {
+ this.handleModeChange(newMode);
+ }
+ });
+ });
+
+ const githubUrlInput = document.getElementById('github-url');
+ if (githubUrlInput && !state.repositoryValidated) {
+ githubUrlInput.addEventListener('input', (e) => {
+ state.githubUrl = e.target.value;
+ state.errors.github = '';
+ });
+
+ githubUrlInput.addEventListener('blur', (e) => this.handleGitHubUrlChange(e.target.value));
+
+ githubUrlInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.handleGitHubUrlChange(e.target.value);
+ }
+ });
+ }
+
+ const orgInput = document.getElementById('org-name');
+ if (orgInput) {
+ orgInput.addEventListener('input', (e) => {
+ state.org = e.target.value.trim();
+ state.errors.site = '';
+ });
+ }
+
+ const siteInput = document.getElementById('site-name');
+ if (siteInput) {
+ siteInput.addEventListener('input', (e) => {
+ state.site = e.target.value.trim();
+ state.errors.site = '';
+ });
+ }
+
+ const startBtn = document.getElementById('start-processing');
+ if (startBtn) {
+ startBtn.addEventListener('click', () => this.handleStartProcessing());
+ }
+
+ blocksHandlers.attachBlocksListeners(this, state);
+ templatesHandler.attachListeners(this, state);
+ iconsHandler.attachListeners(this, state);
+ placeholdersHandler.attachListeners(this, state);
+ aemAssetsHandlers.attachAemAssetsListeners(this, state);
+ translationHandlers.attachTranslationListeners(this, state);
+ universalEditorHandlers.attachUniversalEditorListeners(this, state);
+
+ document.querySelectorAll('.select-pages-btn').forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ const { site } = e.target.dataset;
+ this.openPagePicker(site);
+ });
+ });
+
+ document.querySelectorAll('.remove-page-btn').forEach((btn) => {
+ btn.addEventListener('click', (e) => {
+ const { site, path } = e.target.dataset;
+ this.removePage(site, path);
+ });
+ });
+
+ document.querySelectorAll('.error-modal-close').forEach((btn) => {
+ btn.addEventListener('click', () => this.hideErrorModal());
+ });
+
+ const errorModalOverlay = document.getElementById('error-modal');
+ if (errorModalOverlay) {
+ errorModalOverlay.addEventListener('click', (e) => {
+ if (e.target === errorModalOverlay) {
+ this.hideErrorModal();
+ }
+ });
+ }
+
+ const errorsCard = document.querySelector('.import-card.errors');
+ if (errorsCard && state.processStatus.errors.count > 0) {
+ errorsCard.style.cursor = 'pointer';
+ errorsCard.addEventListener('click', () => this.showErrorModal());
+ }
+ },
+
+ async handleModeChange(newMode) {
+ state.mode = newMode;
+ state.message = '';
+ state.messageType = 'info';
+ clearErrors();
+
+ if (newMode === 'refresh') {
+ resetModeState();
+ this.render();
+ if (state.org && state.site) {
+ await this.handleLoadExistingBlocks();
+ }
+ return;
+ }
+
+ if (newMode === 'setup') {
+ resetModeState(true);
+ }
+
+ this.render();
+ },
+
+ async handleGitHubUrlChange(url) {
+ state.githubUrl = url.trim();
+
+ const parsed = githubOps.parseGitHubURL(state.githubUrl);
+ if (parsed && parsed.org && parsed.repo) {
+ state.org = parsed.org;
+ state.repo = parsed.repo;
+ state.site = parsed.repo;
+
+ await this.validateRepository();
+ }
+ },
+
+ async validateRepository() {
+ state.validating = true;
+ this.render();
+ try {
+ const result = await githubOps.validateRepository(state.org, state.repo, state.githubToken);
+
+ if (!result.valid) {
+ if (result.error === 'rate_limit') {
+ state.needsToken = true;
+ state.errors.github = `GitHub API rate limit exceeded (resets at ${result.resetTime}). Please add a GitHub token to continue, or wait and try again.`;
+ state.validating = false;
+ this.render();
+ return;
+ }
+
+ if (result.error === 'not_found') {
+ state.errors.github = 'Repository not found. Please check the URL and try again.';
+ state.validating = false;
+ this.render();
+ return;
+ }
+
+ if (result.error === 'private' && result.needsToken) {
+ state.needsToken = true;
+ state.errors.github = 'Unable to access repository. If this is a private repository, please enter a GitHub token below.';
+ state.validating = false;
+ this.render();
+ return;
+ }
+
+ state.errors.github = result.error === 'private' ? 'Unable to access repository with provided token.' : result.error;
+ state.validating = false;
+ this.render();
+ return;
+ }
+
+ state.repositoryValidated = true;
+ state.needsToken = false;
+
+ state.validating = false;
+ this.render();
+ await this.discoverBlocks();
+ } catch (error) {
+ let errorMsg = error.message;
+ if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
+ errorMsg = 'Please enter a valid GitHub repository URL.';
+ } else if (errorMsg.includes('Failed to fetch') || errorMsg.includes('NetworkError')) {
+ errorMsg = 'Network error. Please check your connection and try again.';
+ }
+
+ state.message = errorMsg;
+ state.messageType = 'error';
+ state.validating = false;
+ this.render();
+ }
+ },
+
+ async handleValidateWithToken() {
+ const tokenInput = document.getElementById('github-token');
+ const saveCheckbox = document.getElementById('save-token');
+ const token = tokenInput?.value.trim();
+
+ if (!token) {
+ state.errors.github = 'Please enter a GitHub token';
+ this.render();
+ return;
+ }
+
+ if (saveCheckbox?.checked) {
+ TokenStorage.set(token);
+ }
+
+ state.githubToken = token;
+ await this.validateRepository();
+ },
+
+ handleClearToken() {
+ TokenStorage.clear();
+ state.githubToken = null;
+ state.message = 'Saved token cleared';
+ state.messageType = 'success';
+ this.render();
+ },
+
+ async handleLoadExistingBlocks() {
+ if (!state.org || !state.site) {
+ state.errors.site = 'Please enter both organization and site name';
+ this.render();
+ return;
+ }
+
+ state.discovering = true;
+ this.render();
+
+ try {
+ const blocks = await libraryOps.fetchExistingBlocks(state.org, state.site);
+
+ if (blocks.length === 0) {
+ state.errors.site = 'No library found at this location. Please run "Library Setup" first to create the library.';
+ state.discovering = false;
+ this.render();
+ return;
+ }
+
+ state.blocks = blocks.map((block) => ({
+ ...block,
+ isNew: false,
+ }));
+ state.blocksDiscovered = true;
+ state.discovering = false;
+ state.selectedBlocks = new Set(blocks.map((b) => b.name));
+ state.errors = {
+ github: '', site: '', blocks: '', pages: '',
+ };
+ this.render();
+ } catch (error) {
+ state.errors.site = `Unable to load library: ${error.message}. Please run "Library Setup" first to create the library.`;
+ state.discovering = false;
+ this.render();
+ }
+ },
+
+ async discoverBlocks() {
+ state.discovering = true;
+ this.render();
+ try {
+ const blocks = await githubOps.discoverBlocks(state.org, state.repo, state.githubToken);
+
+ const existingBlocksJSON = await daApi.fetchBlocksJSON(state.org, state.site);
+
+ const existingBlockNames = new Set(
+ existingBlocksJSON?.data?.data?.map((b) => {
+ const pathParts = b.path.split('/');
+ return pathParts[pathParts.length - 1]; // Get last part of path (kebab-case name)
+ }) || [],
+ );
+
+ state.blocks = blocks.map((block) => ({
+ ...block,
+ isNew: !existingBlockNames.has(block.name),
+ }));
+
+ state.blocksDiscovered = true;
+ state.discovering = false;
+
+ state.selectedBlocks = new Set(blocks.map((b) => b.name));
+
+ const libraryCheck = await libraryOps.checkLibraryExists(state.org, state.site);
+ state.libraryExists = libraryCheck.exists;
+
+ this.render();
+ } catch (error) {
+ state.errors.github = `Block discovery failed: ${error.message}`;
+ state.discovering = false;
+ this.render();
+ }
+ },
+
+ async handleStartProcessing() {
+ if (!state.daToken) {
+ state.errors.pages = 'DA.live authentication required. This tool must be run from within DA.live.';
+ this.render();
+ return;
+ }
+
+ const siteValid = await this.validateSite(state.org, state.site, state.daToken);
+ if (!siteValid) {
+ state.errors.site = `Site "${state.org}/${state.site}" not found in DA.live. Please verify the site name.`;
+ this.render();
+ return;
+ }
+
+ state.processing = true;
+ const baseStatus = {
+ errors: { count: 0, messages: [] },
+ completed: false,
+ };
+
+ if (state.selectedBlocks.size > 0) {
+ baseStatus.github = { org: state.org, repo: state.repo, status: 'complete' };
+ baseStatus.blocks = { total: state.selectedBlocks.size, status: 'complete' };
+ baseStatus.blockDocs = { created: 0, total: state.selectedBlocks.size, status: 'pending' };
+ }
+
+ if (state.selectedTemplates.length > 0) {
+ baseStatus.templates = { processed: 0, total: state.selectedTemplates.length, status: 'pending' };
+ }
+
+ if (state.selectedIcons.length > 0) {
+ baseStatus.icons = { processed: 0, total: state.selectedIcons.length, status: 'pending' };
+ }
+
+ if (state.selectedPlaceholders.length > 0) {
+ baseStatus.placeholders = { processed: 0, total: state.selectedPlaceholders.length, status: 'pending' };
+ }
+
+ if (state.mode === 'setup') {
+ baseStatus.siteConfig = { status: 'pending', message: '' };
+
+ if (state.selectedBlocks.size > 0) {
+ baseStatus.blocksJson = { status: 'pending', message: '' };
+ }
+ if (state.selectedTemplates.length > 0) {
+ baseStatus.templatesJson = { status: 'pending', message: '' };
+ }
+ if (state.selectedIcons.length > 0) {
+ baseStatus.iconsJson = { status: 'pending', message: '' };
+ }
+ if (state.selectedPlaceholders.length > 0) {
+ baseStatus.placeholdersJson = { status: 'pending', message: '' };
+ }
+ }
+
+ state.processStatus = baseStatus;
+ this.render();
+
+ try {
+ const selectedBlockNames = Array.from(state.selectedBlocks);
+ const sitesWithPages = this.getAllSites().map((site) => ({
+ org: state.org,
+ site,
+ pages: Array.from(state.pageSelections[site] || []),
+ }));
+
+ let githubApi = null;
+ if (state.mode === 'setup' && state.org && state.repo) {
+ githubApi = new GitHubAPI(state.org, state.repo, 'main', state.githubToken);
+ }
+
+ const results = await libraryOps.setupLibrary({
+ org: state.org,
+ site: state.site,
+ blockNames: selectedBlockNames,
+ templates: state.selectedTemplates,
+ icons: state.selectedIcons,
+ placeholders: state.selectedPlaceholders,
+ sitesWithPages,
+ onProgress: (progress) => this.handleProgress(progress),
+ skipSiteConfig: state.mode === 'refresh',
+ githubApi,
+ });
+
+ if (!results.success) {
+ throw new Error(results.error || 'Library setup failed');
+ }
+
+ state.processStatus.completed = true;
+ state.message = '';
+ state.messageType = '';
+ } catch (error) {
+ state.processStatus.errors.count += 1;
+ state.processStatus.errors.messages.push({
+ type: 'general',
+ block: 'N/A',
+ message: error.message,
+ });
+ state.message = `Processing failed: ${error.message}`;
+ state.messageType = 'error';
+ } finally {
+ state.processing = false;
+ this.render();
+ }
+ },
+
+ handleProgress(progress) {
+ if (progress.step === 'register') {
+ if (state.processStatus.siteConfig) {
+ if (progress.status === 'start') {
+ state.processStatus.siteConfig.status = 'processing';
+ state.processStatus.siteConfig.message = 'Registering library...';
+ } else if (progress.status === 'complete') {
+ state.processStatus.siteConfig.status = 'complete';
+ state.processStatus.siteConfig.message = 'Updated Site Config';
+ }
+ }
+ } else if (progress.step === 'generate' && progress.status === 'start') {
+ state.processStatus.blockDocs.status = 'processing';
+ } else if (progress.step === 'upload') {
+ if (progress.status === 'start') {
+ state.processStatus.blockDocs.status = 'processing';
+ } else if (progress.current && progress.total) {
+ state.processStatus.blockDocs.created = progress.current;
+ state.processStatus.blockDocs.status = 'processing';
+ } else if (progress.status === 'complete') {
+ const uploadSuccessCount = progress.uploadResults.filter((r) => r.success).length;
+ const uploadErrorCount = progress.uploadResults.length - uploadSuccessCount;
+
+ state.processStatus.blockDocs.created = uploadSuccessCount;
+ state.processStatus.blockDocs.status = uploadErrorCount > 0 ? 'warning' : 'complete';
+
+ if (uploadErrorCount > 0) {
+ state.processStatus.errors.count += uploadErrorCount;
+ progress.uploadResults
+ .filter((r) => !r.success)
+ .forEach((r) => state.processStatus.errors.messages.push({
+ type: 'upload',
+ block: r.name,
+ message: r.error,
+ }));
+ }
+ }
+ } else if (progress.step === 'blocks-json') {
+ if (state.processStatus.blocksJson) {
+ if (progress.status === 'start') {
+ state.processStatus.blocksJson.status = 'processing';
+ state.processStatus.blocksJson.message = state.libraryExists
+ ? 'Updating...'
+ : 'Creating...';
+ } else if (progress.status === 'complete') {
+ state.processStatus.blocksJson.status = 'complete';
+ state.processStatus.blocksJson.message = state.libraryExists
+ ? 'Updated'
+ : 'Created';
+ }
+ }
+ } else if (progress.step === 'templates-json') {
+ if (state.processStatus.templates) {
+ state.processStatus.templates.status = 'processing';
+ }
+ if (state.processStatus.templatesJson) {
+ if (progress.status === 'start') {
+ state.processStatus.templatesJson.status = 'processing';
+ state.processStatus.templatesJson.message = 'Creating...';
+ } else if (progress.status === 'complete') {
+ state.processStatus.templatesJson.status = 'complete';
+ state.processStatus.templatesJson.message = 'Created';
+ if (state.processStatus.templates) {
+ state.processStatus.templates.status = 'complete';
+ state.processStatus.templates.processed = state.selectedTemplates.length;
+ }
+ }
+ }
+ } else if (progress.step === 'icons-json') {
+ if (state.processStatus.icons) {
+ state.processStatus.icons.status = 'processing';
+ }
+ if (state.processStatus.iconsJson) {
+ if (progress.status === 'start') {
+ state.processStatus.iconsJson.status = 'processing';
+ state.processStatus.iconsJson.message = 'Creating...';
+ } else if (progress.status === 'complete') {
+ state.processStatus.iconsJson.status = 'complete';
+ state.processStatus.iconsJson.message = 'Created';
+ if (state.processStatus.icons) {
+ state.processStatus.icons.status = 'complete';
+ state.processStatus.icons.processed = state.selectedIcons.length;
+ }
+ }
+ }
+ } else if (progress.step === 'placeholders-json') {
+ if (state.processStatus.placeholders) {
+ state.processStatus.placeholders.status = 'processing';
+ }
+ if (state.processStatus.placeholdersJson) {
+ if (progress.status === 'start') {
+ state.processStatus.placeholdersJson.status = 'processing';
+ state.processStatus.placeholdersJson.message = 'Creating...';
+ } else if (progress.status === 'complete') {
+ state.processStatus.placeholdersJson.status = 'complete';
+ state.processStatus.placeholdersJson.message = 'Created';
+ if (state.processStatus.placeholders) {
+ state.processStatus.placeholders.status = 'complete';
+ state.processStatus.placeholders.processed = state.selectedPlaceholders.length;
+ }
+ }
+ }
+ }
+
+ this.render();
+ },
+
+ toggleAllBlocks() {
+ blocksHandlers.toggleAllBlocks(state, () => this.render());
+ },
+
+ selectNewBlocksOnly() {
+ blocksHandlers.selectNewBlocksOnly(state, () => this.render());
+ },
+
+ getAllSites() {
+ return [state.site];
+ },
+
+ async validateSite(org, site, token) {
+ return pagePickerHandlers.validateSite(org, site, token);
+ },
+
+ async openPagePicker(site, mode = 'pages') {
+ await pagePickerHandlers.openPagePicker(this, state, site, mode);
+ },
+
+ renderPagePickerModal() {
+ pagePickerHandlers.renderPagePickerModal(this, state);
+ },
+
+ attachPagePickerListeners() {
+ pagePickerHandlers.attachPagePickerListeners(this, state);
+ },
+
+ closePagePicker() {
+ pagePickerHandlers.closePagePicker(state);
+ },
+
+ confirmPageSelection() {
+ pagePickerHandlers.confirmPageSelection(this, state);
+ },
+
+ removePage(site, path) {
+ pagePickerHandlers.removePage(state, () => this.render(), site, path);
+ },
+
+ handleAddTemplate() {
+ templatesHandler.handleAdd(state, () => this.render());
+ },
+
+ handleRemoveTemplate(index) {
+ templatesHandler.handleRemove(state, () => this.render(), index);
+ },
+
+ handleEditExistingTemplate(index) {
+ templatesHandler.handleEditExisting(state, () => this.render(), index);
+ },
+
+ handleRemoveExistingTemplate(index) {
+ templatesHandler.handleRemoveExisting(state, () => this.render(), index);
+ },
+
+ handleCancelEditTemplate() {
+ templatesHandler.handleCancelEdit(state, () => this.render());
+ },
+
+ handleAddIcon() {
+ iconsHandler.handleAdd(state, () => this.render());
+ },
+
+ handleRemoveIcon(index) {
+ iconsHandler.handleRemove(state, () => this.render(), index);
+ },
+
+ handleEditExistingIcon(index) {
+ iconsHandler.handleEditExisting(state, () => this.render(), index);
+ },
+
+ handleRemoveExistingIcon(index) {
+ iconsHandler.handleRemoveExisting(state, () => this.render(), index);
+ },
+
+ handleCancelEditIcon() {
+ iconsHandler.handleCancelEdit(state, () => this.render());
+ },
+
+ handleAddPlaceholder() {
+ placeholdersHandler.handleAdd(state, () => this.render());
+ },
+
+ handleRemovePlaceholder(index) {
+ placeholdersHandler.handleRemove(state, () => this.render(), index);
+ },
+
+ handleEditExistingPlaceholder(index) {
+ placeholdersHandler.handleEditExisting(state, () => this.render(), index);
+ },
+
+ handleRemoveExistingPlaceholder(index) {
+ placeholdersHandler.handleRemoveExisting(state, () => this.render(), index);
+ },
+
+ handleCancelEditPlaceholder() {
+ placeholdersHandler.handleCancelEdit(state, () => this.render());
+ },
+
+ handleSaveAemConfig() {
+ aemAssetsHandlers.handleSaveAemConfig(state, () => this.render(), daApi);
+ },
+
+ handleVerifyAemUrl() {
+ aemAssetsHandlers.handleVerifyAemUrl(state, () => this.render());
+ },
+
+ handleSaveTranslationConfig() {
+ translationHandlers.handleSaveTranslationConfig(state, () => this.render(), daApi);
+ },
+
+ handleSaveUeConfig() {
+ universalEditorHandlers.handleSaveUeConfig(state, () => this.render(), daApi);
+ },
+
+ showErrorModal() {
+ const modal = document.getElementById('error-modal');
+ if (modal) {
+ modal.style.display = 'flex';
+ }
+ },
+
+ hideErrorModal() {
+ const modal = document.getElementById('error-modal');
+ if (modal) {
+ modal.style.display = 'none';
+ }
+ },
+};
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => app.init());
+} else {
+ app.init();
+}
+
+export default app;
diff --git a/tools/admin/app/router.js b/tools/admin/app/router.js
new file mode 100644
index 0000000..ba2cda7
--- /dev/null
+++ b/tools/admin/app/router.js
@@ -0,0 +1,24 @@
+export function initRouter(viewCallbacks) {
+ function handleRoute() {
+ const hash = window.location.hash.slice(1) || '/blocks';
+ const route = hash.split('/')[1];
+
+ if (viewCallbacks[route]) {
+ viewCallbacks[route]();
+ } else {
+ window.location.hash = '#/blocks';
+ }
+ }
+
+ window.addEventListener('hashchange', handleRoute);
+ handleRoute();
+}
+
+export function getCurrentRoute() {
+ const hash = window.location.hash.slice(1) || '/blocks';
+ return hash.split('/')[1];
+}
+
+export function navigate(route) {
+ window.location.hash = `#/${route}`;
+}
diff --git a/tools/admin/app/state.js b/tools/admin/app/state.js
new file mode 100644
index 0000000..6efd05a
--- /dev/null
+++ b/tools/admin/app/state.js
@@ -0,0 +1,134 @@
+const state = {
+ mode: 'setup',
+ daToken: null,
+ githubUrl: '',
+ githubToken: null,
+ needsToken: false,
+ org: '',
+ repo: '',
+ site: '',
+ validating: false,
+ repositoryValidated: false,
+ selectedContentTypes: new Set(['blocks']),
+ blocks: [],
+ selectedBlocks: new Set(),
+ discovering: false,
+ blocksDiscovered: false,
+ libraryExists: false,
+ existingTemplates: [],
+ selectedTemplates: [],
+ templateForm: { name: '', path: '' },
+ editingTemplateIndex: -1,
+ loadingTemplates: false,
+ templateSearchQuery: '',
+ existingIcons: [],
+ selectedIcons: [],
+ iconForm: { name: '', path: '' },
+ editingIconIndex: -1,
+ loadingIcons: false,
+ iconSearchQuery: '',
+ existingPlaceholders: [],
+ selectedPlaceholders: [],
+ placeholderForm: { key: '', value: '' },
+ editingPlaceholderIndex: -1,
+ loadingPlaceholders: false,
+ placeholderSearchQuery: '',
+ aemAssetsConfig: {
+ repositoryId: '',
+ prodOrigin: '',
+ imageType: false,
+ renditionsSelect: false,
+ dmDelivery: false,
+ smartCropSelect: false,
+ },
+ loadingAemConfig: false,
+ validatingAemUrl: false,
+ translationConfig: {
+ translateBehavior: 'overwrite',
+ translateStaging: 'off',
+ rolloutBehavior: 'overwrite',
+ },
+ loadingTranslationConfig: false,
+ universalEditorConfig: {
+ editorPath: '',
+ },
+ loadingUeConfig: false,
+ showPagePicker: false,
+ pagePickerMode: '',
+ currentSite: '',
+ pageSearchQuery: '',
+ allPages: [],
+ loadingPages: false,
+ loadedFolders: {},
+ pageSelections: {},
+ processing: false,
+ processStatus: {
+ github: { org: '', repo: '', status: 'pending' },
+ blocks: {
+ processed: 0, total: 0, status: 'idle', errors: [],
+ },
+ templates: {
+ processed: 0, total: 0, status: 'idle', errors: [],
+ },
+ icons: {
+ processed: 0, total: 0, status: 'idle', errors: [],
+ },
+ placeholders: {
+ processed: 0, total: 0, status: 'idle', errors: [],
+ },
+ siteConfig: { status: 'pending', message: '' },
+ blocksJson: { status: 'pending', message: '' },
+ templatesJson: { status: 'pending', message: '' },
+ iconsJson: { status: 'pending', message: '' },
+ placeholdersJson: { status: 'pending', message: '' },
+ errors: { count: 0, messages: [] },
+ },
+ message: '',
+ messageType: 'info',
+ errors: {
+ github: '',
+ site: '',
+ blocks: '',
+ templates: '',
+ icons: '',
+ placeholders: '',
+ pages: '',
+ aemAssets: '',
+ },
+};
+
+export function resetModeState(includeLibraryExists = false) {
+ Object.assign(state, {
+ repositoryValidated: false,
+ blocksDiscovered: false,
+ needsToken: false,
+ githubUrl: '',
+ blocks: [],
+ processing: false,
+ validating: false,
+ discovering: false,
+ pageSelections: {},
+ });
+ state.selectedBlocks.clear();
+
+ if (includeLibraryExists) {
+ state.libraryExists = false;
+ }
+}
+
+export function clearErrors() {
+ state.errors = {
+ github: '',
+ site: '',
+ blocks: '',
+ templates: '',
+ icons: '',
+ placeholders: '',
+ pages: '',
+ aemAssets: '',
+ translation: '',
+ universalEditor: '',
+ };
+}
+
+export default state;
diff --git a/tools/admin/app/templates.js b/tools/admin/app/templates.js
new file mode 100644
index 0000000..cecb16c
--- /dev/null
+++ b/tools/admin/app/templates.js
@@ -0,0 +1,1456 @@
+import {
+ getLibraryBlocksURL,
+} from '../config.js';
+
+export function navTemplate(currentRoute) {
+ const sections = [
+ {
+ title: 'Library',
+ routes: [
+ { id: 'blocks', label: 'Blocks', enabled: true },
+ { id: 'templates', label: 'Templates', enabled: true },
+ { id: 'icons', label: 'Icons', enabled: true },
+ { id: 'placeholders', label: 'Placeholders', enabled: true },
+ ],
+ },
+ {
+ title: 'Integrations',
+ routes: [
+ { id: 'aem-assets', label: 'AEM Assets', enabled: true },
+ { id: 'translation', label: 'Translation', enabled: true },
+ { id: 'universal-editor', label: 'Universal Editor', enabled: true },
+ ],
+ },
+ ];
+
+ return `
+
+ `;
+}
+
+export function layoutTemplate(currentRoute, content) {
+ return `
+
+ ${navTemplate(currentRoute)}
+
+ ${content}
+
+
+ `;
+}
+
+export function appTemplate(content) {
+ return `
+
+ `;
+}
+
+export function messageTemplate(message, type) {
+ if (!message) return '';
+ return `
+
+ ${message}
+
+ `;
+}
+
+export function sectionHeaderTemplate({ title, description, docsUrl }) {
+ return `
+
+ `;
+}
+
+export function modeToggleTemplate({ currentMode }) {
+ return `
+
+
+ Library Setup
+
+
+ Update Examples
+
+
+ `;
+}
+
+export function contentTypeTogglesTemplate({ selectedTypes }) {
+ const types = [
+ { id: 'blocks', label: 'Blocks' },
+ { id: 'templates', label: 'Templates' },
+ { id: 'icons', label: 'Icons' },
+ { id: 'placeholders', label: 'Placeholders' },
+ ];
+
+ return `
+
+
+ ${types.map((type) => `
+
+
+ ${type.label}
+
+ `).join('')}
+
+
+ `;
+}
+
+export function githubSectionTemplate({
+ isValidated,
+ validating,
+ githubUrl,
+ message,
+}) {
+ return `
+
+ `;
+}
+
+export function tokenInputTemplate({ hasSavedToken }) {
+ return `
+
+ `;
+}
+
+export function siteSectionTemplate({
+ org,
+ site,
+ message,
+ mode = 'setup',
+}) {
+ const isRefreshMode = mode === 'refresh';
+
+ return `
+
+ `;
+}
+
+export function blocksListTemplate({
+ blocks,
+ selectedBlocks,
+ message,
+}) {
+ const selectedCount = selectedBlocks.size;
+ const totalCount = blocks.length;
+ const newCount = blocks.filter((b) => b.isNew).length;
+ const newCountText = newCount > 0 ? ` (${newCount} new)` : '';
+
+ return `
+
+ `;
+}
+
+export function pagesSelectionTemplate({
+ allSites,
+ pageSelections,
+ message,
+ daToken,
+ org,
+ mode = 'setup',
+}) {
+ const isRefreshMode = mode === 'refresh';
+
+ return `
+
+ `;
+}
+
+export function startButtonTemplate({ mode, disabled = false, processing = false }) {
+ let buttonText = 'Set Up Library';
+ if (processing) {
+ buttonText = 'Processing...';
+ } else if (mode === 'refresh') {
+ buttonText = 'Update Examples';
+ }
+
+ return `
+
+ `;
+}
+
+export function initialStatusTemplate({
+ org,
+ repo,
+ blocksCount,
+ mode = 'setup',
+ libraryExists = false,
+}) {
+ return `
+
+ `;
+}
+
+export function processingTemplate({
+ processStatus,
+}) {
+ return `
+
+ `;
+}
+
+export function pagePickerModalTemplate({
+ site,
+ items,
+ selectedPages,
+ loading,
+ mode = 'pages',
+}) {
+ const modalTitles = {
+ icons: 'Select Icons',
+ templates: 'Select Template',
+ pages: 'Select Pages',
+ };
+ const modalTitle = modalTitles[mode] || 'Select Pages';
+
+ const isSingleSelect = mode === 'templates' || mode === 'icons';
+ const inputType = isSingleSelect ? 'radio' : 'checkbox';
+ const inputName = isSingleSelect ? 'selected-item' : '';
+
+ const renderItem = (item) => {
+ if (item.ext) {
+ const isSelected = selectedPages.has(item.path);
+ const displayName = item.name.replace(`.${item.ext}`, '');
+ let icon;
+
+ if (item.ext === 'svg') {
+ const iconUrl = `https://content.da.live${item.path}`;
+ icon = ` `;
+ } else {
+ icon = '📄';
+ }
+
+ return `
+
+
+
+ ${icon}
+ ${displayName}
+
+
+ `;
+ }
+ return `
+
+
+ 📁
+ ${item.name}
+ ▶
+
+
+
+ `;
+ };
+
+ return `
+
+
+
${modalTitle} from ${site}
+
+ ${loading ? `
+
+ ` : `
+
+ ${!items || items.length === 0 ? `
+
No pages found
+ ` : items
+ .sort((a, b) => {
+ if (!a.ext && b.ext) return -1;
+ if (a.ext && !b.ext) return 1;
+ return a.name.localeCompare(b.name);
+ })
+ .map(renderItem)
+ .join('')}
+
+
+
+ Cancel
+
+ Confirm (${selectedPages.size} selected)
+
+
+ `}
+
+
+ `;
+}
+
+export function finalStatusTemplate({ processStatus, org, repo }) {
+ return `
+
+ `;
+}
+
+export function errorModalTemplate(errors) {
+ if (!errors || errors.length === 0) return '';
+
+ const uploadErrors = errors.filter((e) => e.type === 'upload');
+ const generalErrors = errors.filter((e) => e.type === 'general');
+
+ const formatErrorStatus = (message) => {
+ const match = message.match(/(\d{3})/);
+ return match ? match[1] : 'Error';
+ };
+
+ const formatErrorType = (message) => {
+ if (message.includes('Upload failed')) return 'Upload Failed';
+ if (message.includes('403')) return 'Permission Denied';
+ if (message.includes('404')) return 'Not Found';
+ return 'Error';
+ };
+
+ return `
+
+
+
+
+ ${generalErrors.length > 0 ? `
+
+
General Errors
+
+
+
+ Error
+
+
+
+ ${generalErrors.map((e) => `
+
+ ${e.message}
+
+ `).join('')}
+
+
+
+ ` : ''}
+
+ ${uploadErrors.length > 0 ? `
+
+
Upload Errors
+
+
+
+ Block
+ Status
+ Error
+
+
+
+ ${uploadErrors.map((e) => `
+
+ ${e.block}
+ ${formatErrorStatus(e.message)}
+ ${formatErrorType(e.message)}
+
+ `).join('')}
+
+
+
+ ` : ''}
+
+
+
+ `;
+}
+
+export function templatesSectionTemplate({
+ existingTemplates = [],
+ templates,
+ templateForm,
+ editingIndex = -1,
+ searchQuery = '',
+ loading = false,
+ message,
+}) {
+ const totalCount = existingTemplates.length;
+ const showSearch = totalCount > 5;
+ const displayPath = templateForm.path
+ ? templateForm.path.replace(/^\/[^/]+\/[^/]+/, '').replace(/\.html$/, '')
+ : '';
+ const isEditing = editingIndex >= 0;
+ const buttonLabel = isEditing ? 'Update' : '+ Add';
+
+ return `
+
+ `;
+}
+
+export function iconsSectionTemplate({
+ existingIcons = [],
+ icons,
+ iconForm,
+ editingIndex = -1,
+ searchQuery = '',
+ loading = false,
+ message,
+}) {
+ const totalCount = existingIcons.length;
+ const showSearch = totalCount > 5;
+ const displayPath = iconForm.path
+ ? iconForm.path.replace(/^\/[^/]+\/[^/]+/, '').replace(/\.svg$/, '')
+ : '';
+ const isEditing = editingIndex >= 0;
+ const buttonLabel = isEditing ? 'Update' : '+ Add';
+
+ return `
+
+ `;
+}
+
+export function placeholdersSectionTemplate({
+ existingPlaceholders = [],
+ placeholders,
+ placeholderForm,
+ editingIndex = -1,
+ searchQuery = '',
+ loading = false,
+ message,
+}) {
+ const totalCount = existingPlaceholders.length;
+ const showSearch = totalCount > 5;
+ const isEditing = editingIndex >= 0;
+ const buttonLabel = isEditing ? 'Update' : '+ Add';
+
+ return `
+
+ `;
+}
+
+export function aemAssetsSectionTemplate({
+ config,
+ loading = false,
+ validating = false,
+ message,
+}) {
+ return `
+
+ `;
+}
+
+export function translationSectionTemplate({
+ config,
+ loading = false,
+ message,
+}) {
+ return `
+
+ `;
+}
+
+export function universalEditorSectionTemplate({
+ config,
+ loading = false,
+ message,
+}) {
+ return `
+
+ `;
+}
diff --git a/tools/library-setup/config.js b/tools/admin/config.js
similarity index 54%
rename from tools/library-setup/config.js
rename to tools/admin/config.js
index 9a71793..6fc3e44 100644
--- a/tools/library-setup/config.js
+++ b/tools/admin/config.js
@@ -1,6 +1,10 @@
export const LIBRARY_BASE_PATH = 'library';
export const BLOCKS_PATH = 'blocks';
+export const TEMPLATES_PATH = 'templates';
+export const ICONS_PATH = 'icons';
export const LIBRARY_BLOCKS_PATH = `${LIBRARY_BASE_PATH}/${BLOCKS_PATH}`;
+export const LIBRARY_TEMPLATES_PATH = `${LIBRARY_BASE_PATH}/${TEMPLATES_PATH}`;
+export const LIBRARY_ICONS_PATH = `${LIBRARY_BASE_PATH}/${ICONS_PATH}`;
export const DA_LIVE_BASE = 'https://da.live';
export const DA_LIVE_EDIT_BASE = `${DA_LIVE_BASE}/edit`;
@@ -26,3 +30,23 @@ export function getContentBlockPath(org, site, blockName) {
export function getBlocksJSONPath(org, site) {
return `${org}/${site}/${LIBRARY_BASE_PATH}/blocks.json`;
}
+
+export function getTemplatesJSONPath(org, site) {
+ return `${org}/${site}/${LIBRARY_BASE_PATH}/templates.json`;
+}
+
+export function getIconsJSONPath(org, site) {
+ return `${org}/${site}/${LIBRARY_BASE_PATH}/icons.json`;
+}
+
+export function getPlaceholdersJSONPath(org, site) {
+ return `${org}/${site}/placeholders.json`;
+}
+
+export function getContentTemplatePath(org, site, templateName) {
+ return `${CONTENT_DA_LIVE_BASE}/${org}/${site}/${LIBRARY_TEMPLATES_PATH}/${templateName}`;
+}
+
+export function getContentIconPath(org, site, iconName) {
+ return `${CONTENT_DA_LIVE_BASE}/${org}/${site}/${LIBRARY_ICONS_PATH}/${iconName}.svg`;
+}
diff --git a/tools/library-setup/images/refresh-mode.png b/tools/admin/images/refresh-mode.png
similarity index 100%
rename from tools/library-setup/images/refresh-mode.png
rename to tools/admin/images/refresh-mode.png
diff --git a/tools/library-setup/images/setup-mode.png b/tools/admin/images/setup-mode.png
similarity index 100%
rename from tools/library-setup/images/setup-mode.png
rename to tools/admin/images/setup-mode.png
diff --git a/tools/library-setup/operations/github.js b/tools/admin/operations/github.js
similarity index 100%
rename from tools/library-setup/operations/github.js
rename to tools/admin/operations/github.js
diff --git a/tools/admin/operations/icons.js b/tools/admin/operations/icons.js
new file mode 100644
index 0000000..5ef5322
--- /dev/null
+++ b/tools/admin/operations/icons.js
@@ -0,0 +1,52 @@
+import { fetchIconsJSON, updateIconsJSON } from '../utils/da-api.js';
+import { mergeLibraryItems } from './library-items.js';
+
+// eslint-disable-next-line import/prefer-default-export
+export async function updateLibraryIconsJSON(org, site, icons) {
+ const existingJSON = await fetchIconsJSON(org, site);
+ const existingData = existingJSON?.data?.data || existingJSON?.data || [];
+
+ const normalizedNew = icons.map((icon) => {
+ let iconPath = icon.path;
+
+ if (iconPath.startsWith('https://')) {
+ return {
+ key: icon.name,
+ icon: iconPath,
+ };
+ }
+
+ const orgSitePrefix = `/${org}/${site}`;
+ if (iconPath.startsWith(orgSitePrefix)) {
+ iconPath = iconPath.substring(orgSitePrefix.length);
+ }
+
+ return {
+ key: icon.name,
+ icon: `https://content.da.live/${org}/${site}${iconPath}`,
+ };
+ });
+
+ const mergeResult = mergeLibraryItems(existingData, normalizedNew, 'key');
+
+ const iconsJSON = {
+ ':version': 3,
+ ':type': 'sheet',
+ total: mergeResult.merged.length,
+ limit: mergeResult.merged.length,
+ offset: 0,
+ data: mergeResult.merged,
+ };
+
+ const updateResult = await updateIconsJSON(org, site, iconsJSON);
+
+ return {
+ ...updateResult,
+ stats: {
+ added: mergeResult.added,
+ skipped: mergeResult.skipped,
+ existing: mergeResult.existing,
+ total: mergeResult.merged.length,
+ },
+ };
+}
diff --git a/tools/admin/operations/library-items-manager.js b/tools/admin/operations/library-items-manager.js
new file mode 100644
index 0000000..60f2be0
--- /dev/null
+++ b/tools/admin/operations/library-items-manager.js
@@ -0,0 +1,54 @@
+import * as daApi from '../utils/da-api.js';
+
+export async function fetchExistingTemplates(org, site) {
+ try {
+ const json = await daApi.fetchTemplatesJSON(org, site);
+ const data = json?.data?.data || json?.data || [];
+ return data.map((item) => ({
+ name: item.key,
+ path: item.value,
+ }));
+ } catch (error) {
+ return [];
+ }
+}
+
+export async function fetchExistingIcons(org, site) {
+ try {
+ const json = await daApi.fetchIconsJSON(org, site);
+ const data = json?.data?.data || json?.data || [];
+ return data.map((item) => ({
+ name: item.key,
+ path: item.icon,
+ }));
+ } catch (error) {
+ return [];
+ }
+}
+
+export async function fetchExistingPlaceholders(org, site) {
+ try {
+ const json = await daApi.fetchPlaceholdersJSON(org, site);
+ const data = json?.data?.data || json?.data || [];
+ return data.map((item) => ({
+ key: item.value,
+ value: item.key,
+ }));
+ } catch (error) {
+ return [];
+ }
+}
+
+export function filterItems(items, query, type) {
+ if (!query.trim()) return items;
+
+ const lowerQuery = query.toLowerCase();
+
+ if (type === 'placeholders') {
+ return items.filter((item) => item.key.toLowerCase().includes(lowerQuery)
+ || item.value.toLowerCase().includes(lowerQuery));
+ }
+
+ return items.filter((item) => item.name.toLowerCase().includes(lowerQuery)
+ || item.path.toLowerCase().includes(lowerQuery));
+}
diff --git a/tools/admin/operations/library-items.js b/tools/admin/operations/library-items.js
new file mode 100644
index 0000000..abacfb2
--- /dev/null
+++ b/tools/admin/operations/library-items.js
@@ -0,0 +1,40 @@
+export function normalizeIdentifier(str) {
+ return str.toLowerCase().trim().replace(/\s+/g, '-');
+}
+
+export function mergeLibraryItems(existingItems, newItems, identifierKey) {
+ if (!existingItems || existingItems.length === 0) {
+ return {
+ merged: newItems,
+ added: newItems.length,
+ skipped: 0,
+ existing: 0,
+ };
+ }
+
+ const existingMap = new Map(
+ existingItems.map((item) => [
+ normalizeIdentifier(item[identifierKey]),
+ item,
+ ]),
+ );
+
+ const newItemsSet = new Set(
+ newItems.map((item) => normalizeIdentifier(item[identifierKey])),
+ );
+
+ const preserved = existingItems.filter(
+ (item) => !newItemsSet.has(normalizeIdentifier(item[identifierKey])),
+ );
+
+ const itemsToAdd = newItems.filter(
+ (item) => !existingMap.has(normalizeIdentifier(item[identifierKey])),
+ );
+
+ return {
+ merged: [...preserved, ...itemsToAdd],
+ added: itemsToAdd.length,
+ skipped: newItems.length - itemsToAdd.length,
+ existing: existingItems.length,
+ };
+}
diff --git a/tools/library-setup/operations/library.js b/tools/admin/operations/library.js
similarity index 50%
rename from tools/library-setup/operations/library.js
rename to tools/admin/operations/library.js
index e701a41..1adb3e3 100644
--- a/tools/library-setup/operations/library.js
+++ b/tools/admin/operations/library.js
@@ -154,6 +154,9 @@ export async function setupLibrary({
org,
site,
blockNames,
+ templates = [],
+ icons = [],
+ placeholders = [],
sitesWithPages = [],
onProgress,
skipSiteConfig = false,
@@ -176,54 +179,120 @@ export async function setupLibrary({
onProgress?.({ step: 'register', status: 'complete', registration });
}
- let examplesByBlock = {};
- const totalPages = sitesWithPages.reduce((sum, s) => sum + s.pages.length, 0);
- if (totalPages > 0) {
- onProgress?.({ step: 'extract', status: 'start', totalPages });
- examplesByBlock = await extractBlockExamples(blockNames, sitesWithPages, onProgress);
- results.steps.push({ name: 'extract', success: true });
- onProgress?.({ step: 'extract', status: 'complete' });
+ if (blockNames.length > 0) {
+ let examplesByBlock = {};
+ const totalPages = sitesWithPages.reduce((sum, s) => sum + s.pages.length, 0);
+ if (totalPages > 0) {
+ onProgress?.({ step: 'extract', status: 'start', totalPages });
+ examplesByBlock = await extractBlockExamples(blockNames, sitesWithPages, onProgress);
+ results.steps.push({ name: 'extract', success: true });
+ onProgress?.({ step: 'extract', status: 'complete' });
+ }
+
+ let blocksToProcess = blockNames;
+ if (skipSiteConfig && totalPages > 0) {
+ blocksToProcess = blockNames.filter((name) => {
+ const examples = examplesByBlock[name] || [];
+ return examples.length > 0;
+ });
+
+ if (blocksToProcess.length === 0) {
+ throw new Error('No blocks found in the selected sample pages. Please select pages that contain the blocks you want to update.');
+ }
+ }
+
+ onProgress?.({ step: 'generate', status: 'start', totalBlocks: blocksToProcess.length });
+ let discoveredBlocks = [];
+ if (githubApi) {
+ discoveredBlocks = await githubApi.discoverBlocks();
+ }
+ const blocksToUpload = await generateBlockDocs(
+ blocksToProcess,
+ examplesByBlock,
+ githubApi,
+ discoveredBlocks,
+ );
+ results.steps.push({ name: 'generate', success: true });
+ onProgress?.({ step: 'generate', status: 'complete' });
+
+ onProgress?.({ step: 'upload', status: 'start' });
+ const uploadResults = await uploadBlockDocs(org, site, blocksToUpload, onProgress);
+ results.steps.push({ name: 'upload', success: true, results: uploadResults });
+ onProgress?.({ step: 'upload', status: 'complete', uploadResults });
+
+ if (!skipSiteConfig) {
+ onProgress?.({ step: 'blocks-json', status: 'start' });
+ const blocksJsonResult = await updateLibraryBlocksJSON(org, site, blockNames);
+ if (!blocksJsonResult.success) {
+ throw new Error(`Failed to update blocks.json: ${blocksJsonResult.error}`);
+ }
+ results.steps.push({ name: 'blocks-json', success: true });
+ onProgress?.({ step: 'blocks-json', status: 'complete' });
+ }
}
- let blocksToProcess = blockNames;
- if (skipSiteConfig && totalPages > 0) {
- blocksToProcess = blockNames.filter((name) => {
- const examples = examplesByBlock[name] || [];
- return examples.length > 0;
- });
+ if (templates.length > 0) {
+ const { updateLibraryTemplatesJSON } = await import('./templates.js');
+ const { registerTemplatesInConfig } = await import('../utils/da-api.js');
- if (blocksToProcess.length === 0) {
- throw new Error('No blocks found in the selected sample pages. Please select pages that contain the blocks you want to update.');
+ onProgress?.({ step: 'templates-json', status: 'start' });
+ const templatesJsonResult = await updateLibraryTemplatesJSON(org, site, templates);
+ if (!templatesJsonResult.success) {
+ throw new Error(`Failed to update templates.json: ${templatesJsonResult.error}`);
+ }
+ results.steps.push({ name: 'templates-json', success: true, stats: templatesJsonResult.stats });
+ onProgress?.({ step: 'templates-json', status: 'complete' });
+
+ if (!skipSiteConfig) {
+ const registerResult = await registerTemplatesInConfig(org, site);
+ if (!registerResult.success) {
+ throw new Error(`Failed to register templates in config: ${registerResult.error}`);
+ }
}
}
- onProgress?.({ step: 'generate', status: 'start', totalBlocks: blocksToProcess.length });
- let discoveredBlocks = [];
- if (githubApi) {
- discoveredBlocks = await githubApi.discoverBlocks();
+ if (icons.length > 0) {
+ const { updateLibraryIconsJSON } = await import('./icons.js');
+ const { registerIconsInConfig } = await import('../utils/da-api.js');
+
+ onProgress?.({ step: 'icons-json', status: 'start' });
+ const iconsJsonResult = await updateLibraryIconsJSON(org, site, icons);
+ if (!iconsJsonResult.success) {
+ throw new Error(`Failed to update icons.json: ${iconsJsonResult.error}`);
+ }
+ results.steps.push({ name: 'icons-json', success: true, stats: iconsJsonResult.stats });
+ onProgress?.({ step: 'icons-json', status: 'complete' });
+
+ if (!skipSiteConfig) {
+ const registerResult = await registerIconsInConfig(org, site);
+ if (!registerResult.success) {
+ throw new Error(`Failed to register icons in config: ${registerResult.error}`);
+ }
+ }
}
- const blocksToUpload = await generateBlockDocs(
- blocksToProcess,
- examplesByBlock,
- githubApi,
- discoveredBlocks,
- );
- results.steps.push({ name: 'generate', success: true });
- onProgress?.({ step: 'generate', status: 'complete' });
-
- onProgress?.({ step: 'upload', status: 'start' });
- const uploadResults = await uploadBlockDocs(org, site, blocksToUpload, onProgress);
- results.steps.push({ name: 'upload', success: true, results: uploadResults });
- onProgress?.({ step: 'upload', status: 'complete', uploadResults });
- if (!skipSiteConfig) {
- onProgress?.({ step: 'blocks-json', status: 'start' });
- const blocksJsonResult = await updateLibraryBlocksJSON(org, site, blockNames);
- if (!blocksJsonResult.success) {
- throw new Error(`Failed to update blocks.json: ${blocksJsonResult.error}`);
+ if (placeholders.length > 0) {
+ const { updateLibraryPlaceholdersJSON } = await import('./placeholders.js');
+ const { registerPlaceholdersInConfig } = await import('../utils/da-api.js');
+
+ onProgress?.({ step: 'placeholders-json', status: 'start' });
+ const placeholdersJsonResult = await updateLibraryPlaceholdersJSON(org, site, placeholders);
+ if (!placeholdersJsonResult.success) {
+ throw new Error(`Failed to update placeholders.json: ${placeholdersJsonResult.error}`);
+ }
+ results.steps.push({
+ name: 'placeholders-json',
+ success: true,
+ stats: placeholdersJsonResult.stats,
+ });
+ onProgress?.({ step: 'placeholders-json', status: 'complete' });
+
+ if (!skipSiteConfig) {
+ const registerResult = await registerPlaceholdersInConfig(org, site);
+ if (!registerResult.success) {
+ throw new Error(`Failed to register placeholders in config: ${registerResult.error}`);
+ }
}
- results.steps.push({ name: 'blocks-json', success: true });
- onProgress?.({ step: 'blocks-json', status: 'complete' });
}
return results;
diff --git a/tools/library-setup/operations/pages.js b/tools/admin/operations/pages.js
similarity index 93%
rename from tools/library-setup/operations/pages.js
rename to tools/admin/operations/pages.js
index 5d18d36..f60d590 100644
--- a/tools/library-setup/operations/pages.js
+++ b/tools/admin/operations/pages.js
@@ -44,9 +44,7 @@ export async function loadFolderContents(org, site, path) {
return [];
}
- const result = items.filter((item) => !item.ext || item.ext === 'html');
-
- return result;
+ return items;
} catch (error) {
return [];
}
diff --git a/tools/admin/operations/placeholders.js b/tools/admin/operations/placeholders.js
new file mode 100644
index 0000000..0f3d535
--- /dev/null
+++ b/tools/admin/operations/placeholders.js
@@ -0,0 +1,36 @@
+import { fetchPlaceholdersJSON, updatePlaceholdersJSON } from '../utils/da-api.js';
+import { mergeLibraryItems } from './library-items.js';
+
+// eslint-disable-next-line import/prefer-default-export
+export async function updateLibraryPlaceholdersJSON(org, site, placeholders) {
+ const existingJSON = await fetchPlaceholdersJSON(org, site);
+ const existingData = existingJSON?.data?.data || existingJSON?.data || [];
+
+ const normalizedNew = placeholders.map((p) => ({
+ key: p.value,
+ value: p.key,
+ }));
+
+ const mergeResult = mergeLibraryItems(existingData, normalizedNew, 'value');
+
+ const placeholdersJSON = {
+ ':version': 3,
+ ':type': 'sheet',
+ total: mergeResult.merged.length,
+ limit: mergeResult.merged.length,
+ offset: 0,
+ data: mergeResult.merged,
+ };
+
+ const updateResult = await updatePlaceholdersJSON(org, site, placeholdersJSON);
+
+ return {
+ ...updateResult,
+ stats: {
+ added: mergeResult.added,
+ skipped: mergeResult.skipped,
+ existing: mergeResult.existing,
+ total: mergeResult.merged.length,
+ },
+ };
+}
diff --git a/tools/admin/operations/templates.js b/tools/admin/operations/templates.js
new file mode 100644
index 0000000..d852b18
--- /dev/null
+++ b/tools/admin/operations/templates.js
@@ -0,0 +1,55 @@
+import {
+ fetchTemplatesJSON,
+ updateTemplatesJSON,
+} from '../utils/da-api.js';
+import { mergeLibraryItems } from './library-items.js';
+
+// eslint-disable-next-line import/prefer-default-export
+export async function updateLibraryTemplatesJSON(org, site, templates) {
+ const existingJSON = await fetchTemplatesJSON(org, site);
+ const existingData = existingJSON?.data?.data || existingJSON?.data || [];
+
+ const normalizedNew = templates.map((template) => {
+ let templatePath = template.path.replace(/\.html$/, '');
+
+ if (templatePath.startsWith('https://')) {
+ return {
+ key: template.name,
+ value: templatePath,
+ };
+ }
+
+ const orgSitePrefix = `/${org}/${site}`;
+ if (templatePath.startsWith(orgSitePrefix)) {
+ templatePath = templatePath.substring(orgSitePrefix.length);
+ }
+
+ return {
+ key: template.name,
+ value: `https://content.da.live/${org}/${site}${templatePath}`,
+ };
+ });
+
+ const mergeResult = mergeLibraryItems(existingData, normalizedNew, 'key');
+
+ const templatesJSON = {
+ ':version': 3,
+ ':type': 'sheet',
+ total: mergeResult.merged.length,
+ limit: mergeResult.merged.length,
+ offset: 0,
+ data: mergeResult.merged,
+ };
+
+ const updateResult = await updateTemplatesJSON(org, site, templatesJSON);
+
+ return {
+ ...updateResult,
+ stats: {
+ added: mergeResult.added,
+ skipped: mergeResult.skipped,
+ existing: mergeResult.existing,
+ total: mergeResult.merged.length,
+ },
+ };
+}
diff --git a/tools/library-setup/library-setup.html b/tools/admin/siteadmin.html
similarity index 74%
rename from tools/library-setup/library-setup.html
rename to tools/admin/siteadmin.html
index 7ea2b4f..e8fb850 100644
--- a/tools/library-setup/library-setup.html
+++ b/tools/admin/siteadmin.html
@@ -3,8 +3,8 @@
- Block Library Setup
-
+ Site Admin
+
diff --git a/tools/admin/styles/admin.css b/tools/admin/styles/admin.css
new file mode 100644
index 0000000..fa45c99
--- /dev/null
+++ b/tools/admin/styles/admin.css
@@ -0,0 +1,541 @@
+/* ========== Base & Variables ========== */
+
+:root {
+ /* Gray Scale */
+ --s2-gray-50: #f9fafb;
+ --s2-gray-100: #f3f4f6;
+ --s2-gray-200: #e5e7eb;
+ --s2-gray-300: #d1d5db;
+ --s2-gray-400: #9ca3af;
+ --s2-gray-500: #6b7280;
+ --s2-gray-600: #4b5563;
+ --s2-gray-700: #374151;
+ --s2-gray-900: #111827;
+
+ /* Blue Scale */
+ --s2-blue-50: #eff6ff;
+ --s2-blue-100: #dbeafe;
+ --s2-blue-200: #bfdbfe;
+ --s2-blue-300: #93c5fd;
+ --s2-blue-500: #3b82f6;
+ --s2-blue-600: #2563eb;
+ --s2-blue-700: #1d4ed8;
+ --s2-blue-900: #1e3a8a;
+
+ /* Green Scale */
+ --s2-green-100: rgb(215 247 225);
+ --s2-green-200: rgb(202 255 164);
+ --s2-green-900: #065f46;
+
+ /* Cyan Scale */
+ --s2-cyan-100: rgb(202 248 250);
+
+ /* Red Scale */
+ --s2-red-50: rgb(255 235 232);
+ --s2-red-100: rgb(255 214 209);
+ --s2-red-200: rgb(255 180 170);
+ --s2-red-600: #dc2626;
+ --s2-red-700: #991b1b;
+
+ /* Yellow Scale */
+ --s2-yellow-50: #fefce8;
+ --s2-yellow-200: #fde047;
+ --s2-yellow-900: #713f12;
+
+ /* Spacing */
+ --spacing-100: 4px;
+ --spacing-200: 8px;
+ --spacing-300: 12px;
+ --spacing-400: 16px;
+ --spacing-500: 24px;
+ --spacing-600: 32px;
+ --spacing-700: 40px;
+ --spacing-800: 48px;
+
+ /* Border Radius */
+ --s2-radius-100: 4px;
+ --s2-radius-200: 8px;
+ --s2-radius-300: 18px;
+
+ /* Typography */
+ --body-font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif;
+ --mono-font-family: 'Roboto Mono', menlo, consolas, 'Liberation Mono', monospace;
+ --s2-font-size-100: 12px;
+ --s2-font-size-200: 14px;
+ --s2-font-size-300: 16px;
+ --s2-font-size-400: 16px;
+ --s2-font-size-600: 24px;
+ --s2-font-size-700: 32px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--body-font-family);
+ color: var(--s2-gray-900);
+ line-height: 1.6;
+ margin: 0;
+ padding: 0;
+}
+
+.siteadmin-container {
+ max-width: 1200px;
+ margin: var(--spacing-800) auto;
+ padding: 0 var(--spacing-400);
+}
+
+.siteadmin-header {
+ margin-bottom: var(--spacing-600);
+
+ & h1 {
+ font-size: 32px;
+ line-height: 1.2;
+ margin: 0 0 var(--spacing-200);
+ }
+
+ & p {
+ font-size: var(--s2-font-size-400);
+ color: var(--s2-gray-600);
+ margin: 0;
+ }
+}
+
+.form-section {
+ background: white;
+ padding: var(--spacing-300) 0;
+ margin-bottom: var(--spacing-300);
+
+ & h2 {
+ font-size: 20px;
+ margin: 0 0 var(--spacing-200);
+ }
+}
+
+.form-section-subtitle {
+ font-size: var(--s2-font-size-200);
+ color: var(--s2-gray-600);
+ margin: 0 0 var(--spacing-500);
+}
+
+.form-row {
+ margin-bottom: var(--spacing-500);
+}
+
+label {
+ display: block;
+ font-weight: 700;
+ font-size: var(--s2-font-size-200);
+ margin-bottom: var(--spacing-200);
+}
+
+.label-hint {
+ font-weight: 400;
+ color: var(--s2-gray-600);
+ font-size: 13px;
+}
+
+input[type="text"],
+input[type="url"] {
+ font-family: var(--body-font-family);
+ display: block;
+ background: var(--s2-gray-50);
+ border: 2px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-100);
+ line-height: 32px;
+ padding: 0 var(--spacing-300);
+ width: 100%;
+ font-size: var(--s2-font-size-200);
+ transition: border-color 0.2s;
+
+ &:focus {
+ outline: none;
+ border-color: var(--s2-blue-900);
+ }
+
+ &[readonly] {
+ background: var(--s2-gray-200);
+ cursor: not-allowed;
+ }
+}
+
+button,
+.button {
+ font-family: var(--body-font-family);
+ font-size: 15px;
+ font-weight: 700;
+ padding: 8px 24px;
+ line-height: 18px;
+ border: 2px solid #000;
+ color: #000;
+ border-radius: var(--s2-radius-300);
+ background: none;
+ cursor: pointer;
+ transition: all 0.2s;
+ text-align: center;
+
+ &:disabled {
+ background-color: #efefef;
+ border: 2px solid #efefef;
+ color: var(--s2-gray-700);
+ cursor: not-allowed;
+ }
+
+ &.accent {
+ background: #3b63fb;
+ border: 2px solid #3b63fb;
+ color: #fff;
+ }
+}
+
+.action,
+button.action {
+ line-height: 1;
+ padding: 4px 8px;
+ font-size: 14px;
+ font-weight: 400;
+ border-radius: var(--s2-radius-100);
+ background: rgb(225 225 225);
+ border: 2px solid rgb(225 225 225);
+ color: #000;
+
+ &:hover {
+ background: rgb(200 200 200);
+ border: 2px solid rgb(200 200 200);
+ }
+}
+
+.button-group {
+ display: flex;
+ gap: var(--spacing-300);
+ margin-top: var(--spacing-500);
+ padding-top: var(--spacing-600);
+ border-top: 1px solid var(--s2-gray-200);
+}
+
+.status-banner {
+ padding: var(--spacing-300) var(--spacing-400);
+ border-radius: var(--s2-radius-100);
+ margin: var(--spacing-400) 0;
+ font-size: var(--s2-font-size-200);
+
+ &.info {
+ background: var(--s2-blue-200);
+ color: var(--s2-blue-900);
+ }
+
+ &.success {
+ background: var(--s2-green-100);
+ color: var(--s2-green-900);
+ }
+
+ &.error {
+ background: var(--s2-red-100);
+ color: #991b1b;
+ }
+
+ &.success {
+ background: var(--s2-green-100);
+ color: #065f46;
+ }
+}
+
+.loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: var(--spacing-600);
+
+ & p {
+ margin-top: var(--spacing-300);
+ color: var(--s2-gray-600);
+ }
+}
+
+.spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--s2-gray-200);
+ border-top-color: var(--s2-blue-900);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.section-header-info {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--spacing-500);
+ padding: var(--spacing-500) var(--spacing-600);
+ background: var(--s2-blue-50);
+ border: 1px solid var(--s2-blue-200);
+ border-radius: var(--s2-radius-200);
+ margin-top: var(--spacing-600);
+ margin-bottom: var(--spacing-600);
+}
+
+.section-header-content {
+ flex: 1;
+}
+
+.section-title {
+ margin: 0 0 var(--spacing-200);
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--s2-gray-900);
+}
+
+.section-description {
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--s2-gray-700);
+}
+
+.section-docs-link {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-200);
+ padding: var(--spacing-300) var(--spacing-400);
+ background: white;
+ border: 1px solid var(--s2-blue-300);
+ border-radius: var(--s2-radius-100);
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--s2-blue-700);
+ text-decoration: none;
+ white-space: nowrap;
+ transition: all 0.2s;
+
+ & svg {
+ flex-shrink: 0;
+ }
+
+ &:hover {
+ background: var(--s2-blue-100);
+ border-color: var(--s2-blue-400);
+ color: var(--s2-blue-800);
+ }
+}
+
+/* ========== Layout ========== */
+
+.app-container {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ min-height: 100vh;
+ gap: 0;
+}
+
+.sidebar {
+ background: var(--s2-gray-50);
+ border-right: 1px solid var(--s2-gray-200);
+ padding: var(--spacing-600) 0;
+ position: sticky;
+ top: 0;
+ height: 100vh;
+ overflow-y: auto;
+
+ & h1 {
+ margin: 0 var(--spacing-600) var(--spacing-600);
+ padding-bottom: var(--spacing-500);
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--s2-gray-900);
+ border-bottom: 2px solid var(--s2-gray-300);
+ }
+}
+
+.main-content {
+ padding: var(--spacing-700);
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+/* ========== Navigation ========== */
+
+.nav-section {
+ margin-bottom: var(--spacing-600);
+ padding-bottom: var(--spacing-500);
+ border-bottom: 1px solid var(--s2-gray-200);
+
+ &:last-child {
+ margin-bottom: 0;
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+}
+
+.nav-section-title {
+ margin: 0 0 var(--spacing-400);
+ padding: 0 var(--spacing-600);
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: var(--s2-gray-400);
+}
+
+.nav-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.nav-item {
+ margin: 0;
+}
+
+.nav-link {
+ display: block;
+ padding: var(--spacing-300) var(--spacing-600);
+ color: var(--s2-gray-700);
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s;
+ border-left: 3px solid transparent;
+ border-radius: 0;
+
+ &:hover {
+ background: var(--s2-gray-100);
+ color: var(--s2-gray-900);
+ }
+
+ &.active {
+ background: var(--s2-blue-50);
+ color: var(--s2-blue-700);
+ font-weight: 600;
+ border-left-color: var(--s2-blue-600);
+ }
+
+ &.disabled {
+ color: var(--s2-gray-400);
+ cursor: not-allowed;
+ opacity: 0.5;
+
+ &:hover {
+ background: transparent;
+ color: var(--s2-gray-400);
+ }
+ }
+}
+
+/* ========== Mode Toggle ========== */
+
+.mode-toggle {
+ display: flex;
+ gap: var(--spacing-200);
+ margin-top: var(--spacing-600);
+ margin-bottom: var(--spacing-600);
+ padding: var(--spacing-200);
+ background: var(--s2-gray-200);
+ border-radius: var(--s2-radius-200);
+ width: fit-content;
+ border: 2px solid var(--s2-gray-200);
+}
+
+.mode-btn {
+ padding: var(--spacing-200) var(--spacing-500);
+ background: transparent;
+ border: 2px solid transparent;
+ border-radius: var(--s2-radius-100);
+ font-size: var(--s2-font-size-200);
+ font-weight: 600;
+ color: var(--s2-gray-600);
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: white;
+ color: var(--s2-gray-900);
+ }
+
+ &.active {
+ background: white;
+ color: #000;
+ font-weight: 700;
+ border: 2px solid #000;
+ box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
+ }
+}
+
+/* ========== Content Toggles ========== */
+
+.content-type-toggles {
+ margin-bottom: var(--spacing-600);
+}
+
+.content-type-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-300);
+}
+
+.content-type-option {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+ padding: var(--spacing-300);
+ background: var(--s2-gray-50);
+ border: 2px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-100);
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background: white;
+ }
+
+ &.selected {
+ background: white;
+ border: 2px solid #000;
+ box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
+ }
+}
+
+.content-type-option input[type="checkbox"] {
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.content-type-label {
+ font-weight: 500;
+ font-size: var(--s2-font-size-200);
+ flex: 1;
+}
+
+/* ========== Responsive ========== */
+
+@media (width <= 900px) {
+ .app-container {
+ grid-template-columns: 1fr;
+ }
+
+ .sidebar {
+ position: static;
+ height: auto;
+ border-right: none;
+ border-bottom: 1px solid var(--s2-gray-200);
+ }
+
+ .main-content {
+ padding: var(--spacing-600);
+ }
+}
+
+@media (width <= 768px) {
+ .section-header-info {
+ flex-direction: column;
+ }
+
+ .section-docs-link {
+ align-self: flex-start;
+ }
+}
diff --git a/tools/admin/styles/blocks-section.css b/tools/admin/styles/blocks-section.css
new file mode 100644
index 0000000..0345a80
--- /dev/null
+++ b/tools/admin/styles/blocks-section.css
@@ -0,0 +1,132 @@
+.blocks-section {
+ padding-top: var(--spacing-600);
+ border-top: 1px solid var(--s2-gray-200);
+}
+
+.blocks-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-400);
+
+ & h2 {
+ margin: 0;
+ display: flex;
+ align-items: baseline;
+ gap: var(--spacing-200);
+ }
+}
+
+.blocks-actions {
+ display: flex;
+ gap: var(--spacing-200);
+}
+
+.heading-annotation {
+ font-size: 16px;
+ font-weight: 400;
+ color: var(--s2-gray-600);
+}
+
+.blocks-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: var(--spacing-300);
+}
+
+.block-item {
+ background: var(--s2-gray-50);
+ border: 2px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-100);
+ padding: var(--spacing-300);
+ transition: all 0.2s;
+
+ &:hover {
+ border-color: var(--s2-blue-900);
+ }
+
+ &.selected {
+ background: var(--s2-blue-200);
+ border-color: var(--s2-blue-900);
+ }
+
+ & label {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+ cursor: pointer;
+ margin: 0;
+ font-weight: 400;
+ }
+
+ & input[type="checkbox"] {
+ cursor: pointer;
+ }
+}
+
+.block-name {
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+ flex: 1;
+}
+
+.block-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ font-size: 11px;
+ font-weight: 600;
+ border-radius: var(--s2-radius-100);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+
+ &.new {
+ background: white;
+ color: var(--s2-gray-900);
+ border: 1px solid var(--s2-gray-200);
+ }
+}
+
+.site-section {
+ margin-bottom: var(--spacing-500);
+ margin-top: var(--spacing-600);
+}
+
+.site-selection {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+}
+
+.selected-pages-list {
+ margin-top: var(--spacing-300);
+}
+
+.selected-page-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--s2-gray-50);
+ padding: var(--spacing-200) var(--spacing-300);
+ border-radius: var(--s2-radius-100);
+ margin-bottom: var(--spacing-200);
+}
+
+.page-path {
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+}
+
+.remove-page-btn {
+ background: transparent;
+ border: none;
+ color: var(--s2-red-600);
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+ width: 24px;
+ height: 24px;
+}
diff --git a/tools/admin/styles/error-modal.css b/tools/admin/styles/error-modal.css
new file mode 100644
index 0000000..cb955f0
--- /dev/null
+++ b/tools/admin/styles/error-modal.css
@@ -0,0 +1,164 @@
+.error-modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgb(0 0 0 / 50%);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+}
+
+.error-modal {
+ background: white;
+ border-radius: 8px;
+ max-width: 600px;
+ width: 90%;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
+}
+
+.error-modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--spacing-400);
+ border-bottom: 1px solid var(--s2-gray-200);
+
+ & h2 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ }
+}
+
+.error-modal-close {
+ background: none;
+ border: none;
+ font-size: 28px;
+ line-height: 1;
+ cursor: pointer;
+ color: var(--s2-gray-600);
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: var(--s2-gray-900);
+ }
+}
+
+.error-modal-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--spacing-400);
+}
+
+.error-section {
+ margin-bottom: var(--spacing-400);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ & h3 {
+ margin: 0 0 var(--spacing-200) 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--s2-gray-900);
+ }
+}
+
+.error-note {
+ margin: 0 0 var(--spacing-200) 0;
+ padding: var(--spacing-200);
+ background: var(--s2-blue-50);
+ border-left: 3px solid var(--s2-blue-900);
+ font-size: 14px;
+ color: var(--s2-gray-700);
+}
+
+.error-modal-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ & li {
+ padding: var(--spacing-300);
+ margin-bottom: var(--spacing-200);
+ background: var(--s2-gray-50);
+ border-radius: 4px;
+ font-size: 14px;
+ line-height: 1.5;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ & strong {
+ color: var(--s2-gray-900);
+ font-weight: 600;
+ }
+ }
+}
+
+.error-modal-footer {
+ padding: var(--spacing-400);
+ border-top: 1px solid var(--s2-gray-200);
+ display: flex;
+ justify-content: flex-end;
+}
+
+.error-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: var(--spacing-300);
+ font-size: 14px;
+
+ & thead {
+ background: var(--s2-gray-50);
+ }
+
+ & th {
+ text-align: left;
+ padding: var(--spacing-300);
+ font-weight: 600;
+ color: var(--s2-gray-900);
+ border-bottom: 2px solid var(--s2-gray-200);
+ }
+
+ & td {
+ padding: var(--spacing-300);
+ border-bottom: 1px solid var(--s2-gray-200);
+ color: var(--s2-gray-700);
+ }
+
+ & tbody tr:last-child td {
+ border-bottom: none;
+ }
+
+ & tbody tr:hover {
+ background: var(--s2-gray-50);
+ }
+
+ & .block-name {
+ font-family: var(--mono-font-family);
+ color: var(--s2-gray-900);
+ font-weight: 500;
+ }
+
+ & .status-code {
+ font-family: var(--mono-font-family);
+ font-weight: 600;
+ color: var(--s2-red-700);
+ text-align: left;
+ width: 80px;
+ }
+}
diff --git a/tools/admin/styles/github-section.css b/tools/admin/styles/github-section.css
new file mode 100644
index 0000000..ea2d878
--- /dev/null
+++ b/tools/admin/styles/github-section.css
@@ -0,0 +1,246 @@
+.repo-input-row {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+}
+
+.org-site-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--spacing-400);
+ margin-bottom: var(--spacing-400);
+}
+
+.input-group {
+ display: flex;
+ flex-direction: column;
+}
+
+.readonly-input {
+ background: var(--s2-gray-100);
+ color: var(--s2-gray-600);
+ cursor: not-allowed;
+}
+
+.site-label {
+ font-size: var(--s2-font-size-300);
+ color: var(--s2-gray-600);
+
+ & strong {
+ color: var(--s2-gray-900);
+ }
+}
+
+.info-banner {
+ background: var(--s2-blue-200);
+ color: var(--s2-blue-900);
+ border-radius: var(--s2-radius-100);
+ padding: var(--spacing-300) var(--spacing-400);
+ margin-bottom: var(--spacing-400);
+ font-size: var(--s2-font-size-200);
+}
+
+.preview-notice {
+ background: var(--s2-blue-100);
+ border-left: 4px solid var(--s2-blue-500);
+ border-radius: var(--s2-radius-100);
+ padding: var(--spacing-400);
+ margin: var(--spacing-500) 0;
+}
+
+.preview-notice-content {
+ font-size: var(--s2-font-size-200);
+ color: var(--s2-gray-900);
+
+ & h3 {
+ margin: 0 0 var(--spacing-200) 0;
+ color: var(--s2-blue-900);
+ font-size: 18px;
+ }
+
+ & p {
+ margin: 0 0 var(--spacing-200) 0;
+ line-height: 1.5;
+ }
+
+ & strong {
+ color: var(--s2-blue-900);
+ }
+}
+
+.preview-notice-link {
+ display: inline-block;
+ margin-top: var(--spacing-100);
+ color: var(--s2-blue-700);
+ font-weight: 600;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.token-input-section {
+ background: var(--s2-gray-50);
+ border: 1px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-200);
+ padding: var(--spacing-400);
+ margin-top: var(--spacing-300);
+}
+
+.token-content {
+ display: grid;
+ grid-template-columns: 60% 40%;
+ gap: var(--spacing-500);
+ align-items: start;
+}
+
+@media (width <= 900px) {
+ .token-content {
+ grid-template-columns: 1fr;
+ }
+
+ .token-instructions {
+ border-left: none !important;
+ border-top: 1px solid var(--s2-gray-300);
+ padding-left: 0 !important;
+ padding-top: var(--spacing-400) !important;
+ }
+}
+
+.token-form {
+ flex: 1;
+
+ & .form-row {
+ margin-bottom: var(--spacing-300);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ & .button-row {
+ display: flex;
+ gap: var(--spacing-300);
+ align-items: center;
+ flex-wrap: wrap;
+ }
+}
+
+.token-instructions {
+ padding-left: var(--spacing-500);
+ border-left: 1px solid var(--s2-gray-300);
+
+ & h4 {
+ margin: 0 0 var(--spacing-200) 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--s2-gray-900);
+ }
+
+ & p {
+ margin: 0 0 var(--spacing-200) 0;
+ font-size: 13px;
+ color: var(--s2-gray-700);
+ }
+
+ & ul {
+ margin: 0 0 var(--spacing-400) 0;
+ padding-left: var(--spacing-400);
+ list-style: none;
+ }
+
+ & li {
+ font-size: 13px;
+ color: var(--s2-gray-700);
+ margin-bottom: var(--spacing-200);
+ padding-left: var(--spacing-300);
+ position: relative;
+
+ &::before {
+ content: "•";
+ position: absolute;
+ left: 0;
+ color: var(--s2-gray-500);
+ }
+
+ & strong {
+ color: var(--s2-gray-900);
+ font-weight: 600;
+ font-family: var(--mono-font-family);
+ font-size: 12px;
+ }
+ }
+
+ & .permission-note {
+ font-size: 12px;
+ color: var(--s2-gray-600);
+ font-style: italic;
+ margin-top: var(--spacing-300);
+ }
+}
+
+.create-token-btn {
+ display: inline-block;
+ font-family: var(--body-font-family);
+ font-size: 15px;
+ font-weight: 700;
+ padding: 8px 24px;
+ line-height: 18px;
+ border: 2px solid #000;
+ color: #000;
+ background: white;
+ border-radius: var(--s2-radius-300);
+ text-decoration: none;
+ cursor: pointer;
+ transition: all 0.2s;
+ text-align: center;
+
+ &:hover {
+ background: rgb(245 245 245);
+ }
+}
+
+.token-input {
+ font-family: var(--mono-font-family);
+ letter-spacing: 0.5px;
+ font-size: var(--s2-font-size-200);
+ line-height: 32px;
+ padding: 0 var(--spacing-300);
+ background: var(--s2-gray-50);
+ border: 2px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-100);
+ width: 100%;
+ transition: border-color 0.2s;
+
+ &:focus {
+ outline: none;
+ border-color: var(--s2-blue-900);
+ }
+
+ &::placeholder {
+ color: var(--s2-gray-400);
+ letter-spacing: 1px;
+ }
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--spacing-200);
+ font-weight: 400;
+ cursor: pointer;
+ margin-bottom: 0;
+
+ & input[type="checkbox"] {
+ margin-top: 2px;
+ cursor: pointer;
+ }
+
+ & span {
+ flex: 1;
+ font-size: 13px;
+ color: var(--s2-gray-700);
+ }
+}
diff --git a/tools/admin/styles/integrations.css b/tools/admin/styles/integrations.css
new file mode 100644
index 0000000..f5278d2
--- /dev/null
+++ b/tools/admin/styles/integrations.css
@@ -0,0 +1,183 @@
+.form-section {
+ margin-bottom: var(--spacing-600);
+}
+
+.aem-assets-section {
+ margin-bottom: var(--spacing-600);
+}
+
+.integration-form {
+ max-width: 700px;
+}
+
+.form-field {
+ margin-bottom: var(--spacing-600);
+
+ & h3 {
+ margin: 0 0 var(--spacing-400);
+ font-size: 16px;
+ }
+}
+
+.form-field label {
+ display: block;
+ margin-bottom: var(--spacing-300);
+ font-weight: 600;
+ color: var(--s2-gray-900);
+
+ & .required {
+ color: var(--s2-red-600);
+ }
+}
+
+.form-field input[type="text"],
+.form-field input[type="url"],
+.form-field select {
+ font-family: var(--body-font-family);
+ display: block;
+ background: var(--s2-gray-50);
+ border: 2px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-100);
+ line-height: 32px;
+ padding: 0 var(--spacing-300);
+ width: 100%;
+ font-size: var(--s2-font-size-200);
+ transition: border-color 0.2s;
+
+ &:focus {
+ outline: none;
+ border-color: var(--s2-blue-900);
+ }
+}
+
+.form-field select {
+ padding: var(--spacing-200) var(--spacing-300);
+ line-height: normal;
+}
+
+.config-select {
+ cursor: pointer;
+}
+
+.field-help {
+ margin: var(--spacing-200) 0 0;
+ font-size: 13px;
+ color: var(--s2-gray-600);
+}
+
+.input-with-button {
+ display: flex;
+ gap: var(--spacing-300);
+
+ & input {
+ flex: 1;
+ }
+
+ & button {
+ flex-shrink: 0;
+ white-space: nowrap;
+ }
+}
+
+.checkbox-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-300);
+}
+
+.checkbox-option {
+ padding: var(--spacing-300);
+ border-radius: var(--s2-radius-100);
+ transition: background 0.2s;
+
+ &:hover {
+ background: var(--s2-gray-50);
+ }
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-300);
+ font-weight: 500;
+ cursor: pointer;
+ margin-bottom: var(--spacing-200);
+
+ & input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ flex-shrink: 0;
+ }
+}
+
+.checkbox-help {
+ margin: 0 0 0 calc(18px + var(--spacing-300));
+ font-size: 13px;
+ color: var(--s2-gray-600);
+ line-height: 1.4;
+}
+
+.form-actions {
+ margin-top: var(--spacing-600);
+ padding-top: var(--spacing-600);
+ border-top: 2px solid var(--s2-gray-200);
+
+ & button.primary {
+ background: var(--s2-blue-600);
+ color: white;
+ padding: var(--spacing-400) var(--spacing-600);
+ border: none;
+ border-radius: var(--s2-radius-200);
+ font-size: 15px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background: var(--s2-blue-700);
+ }
+
+ &:disabled {
+ background: var(--s2-gray-300);
+ color: var(--s2-gray-500);
+ cursor: not-allowed;
+ }
+ }
+}
+
+.info-box {
+ background: var(--s2-blue-50);
+ border: 1px solid var(--s2-blue-200);
+ border-radius: var(--s2-radius-100);
+ padding: var(--spacing-500);
+ margin-top: var(--spacing-600);
+
+ & h4 {
+ margin: 0 0 var(--spacing-300);
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--s2-blue-900);
+ }
+
+ & ul {
+ margin: 0;
+ padding-left: var(--spacing-500);
+ color: var(--s2-blue-800);
+ font-size: 13px;
+ line-height: 1.6;
+ }
+
+ & li {
+ margin-bottom: var(--spacing-200);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.translation-section,
+.universal-editor-section {
+ margin-bottom: var(--spacing-600);
+}
diff --git a/tools/admin/styles/library-items-section.css b/tools/admin/styles/library-items-section.css
new file mode 100644
index 0000000..1d25227
--- /dev/null
+++ b/tools/admin/styles/library-items-section.css
@@ -0,0 +1,456 @@
+/* ========== Generic Library Items Styles ========== */
+
+:root {
+ --library-item-grid: minmax(150px, 200px) 1fr auto;
+ --library-form-grid: minmax(150px, 200px) 1fr auto auto auto;
+ --library-placeholder-grid: minmax(150px, 300px) 1fr auto auto auto;
+}
+
+/* Section Container */
+.templates-section,
+.icons-section,
+.placeholders-section {
+ margin-bottom: var(--spacing-600);
+ padding-top: var(--spacing-600);
+ border-top: 1px solid var(--s2-gray-200);
+}
+
+/* Forms */
+.library-item-form {
+ display: grid;
+ grid-template-columns: var(--library-form-grid);
+ gap: var(--spacing-300);
+ margin-bottom: var(--spacing-400);
+ align-items: end;
+
+ & .input-group {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ & .input-group label {
+ margin-bottom: var(--spacing-200);
+ display: block;
+ }
+
+ & .input-group input[type="text"] {
+ margin: 0;
+ width: 100%;
+ }
+
+ & input[type="text"] {
+ margin: 0;
+ width: 100%;
+ }
+
+ & input[type="text"][readonly] {
+ background: var(--s2-gray-50);
+ color: var(--s2-gray-600);
+ cursor: default;
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+ }
+
+ & button {
+ white-space: nowrap;
+ flex-shrink: 0;
+ }
+
+ & button.action {
+ padding: 8px 16px;
+ line-height: 1.2;
+ }
+
+ & button#add-template,
+ & button#add-icon,
+ & button#add-placeholder {
+ font-size: 15px;
+ font-weight: 700;
+ padding: 8px 24px;
+ border-radius: var(--s2-radius-300);
+ }
+
+ & input.validation-required {
+ border: 2px solid #0078d4;
+ background-color: #f0f8ff;
+ }
+}
+
+/* Specific form classes */
+.templates-form,
+.icons-form,
+.placeholders-form {
+ display: grid;
+ gap: var(--spacing-300);
+ margin-bottom: var(--spacing-400);
+ align-items: end;
+}
+
+.templates-form,
+.icons-form {
+ grid-template-columns: var(--library-form-grid);
+}
+
+.placeholders-form {
+ grid-template-columns: var(--library-placeholder-grid);
+}
+
+.templates-form .input-group,
+.icons-form .input-group,
+.placeholders-form .input-group {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.templates-form .input-group label,
+.icons-form .input-group label,
+.placeholders-form .input-group label {
+ margin-bottom: var(--spacing-200);
+ display: block;
+}
+
+.templates-form .input-group input[type="text"],
+.icons-form .input-group input[type="text"],
+.placeholders-form .input-group input[type="text"] {
+ margin: 0;
+ width: 100%;
+}
+
+.templates-form input[type="text"][readonly],
+.icons-form input[type="text"][readonly],
+.placeholders-form input[type="text"][readonly] {
+ background: var(--s2-gray-50);
+ color: var(--s2-gray-600);
+ cursor: default;
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+}
+
+.templates-form button,
+.icons-form button,
+.placeholders-form button {
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.templates-form button.action,
+.icons-form button.action,
+.placeholders-form button.action {
+ padding: 8px 16px;
+ line-height: 1.2;
+}
+
+.templates-form button#add-template,
+.icons-form button#add-icon,
+.placeholders-form button#add-placeholder {
+ font-size: 15px;
+ font-weight: 700;
+ padding: 8px 24px;
+ border-radius: var(--s2-radius-300);
+}
+
+.templates-form input.validation-required,
+.icons-form input.validation-required,
+.placeholders-form input.validation-required {
+ border: 2px solid #0078d4;
+ background-color: #f0f8ff;
+}
+
+/* Item Lists */
+.library-items-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+/* Remove buttons in forms */
+.library-remove-btn {
+ background: transparent;
+ border: none;
+ color: var(--s2-red-600);
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+
+/* Item Display */
+.library-item {
+ display: grid;
+ grid-template-columns: var(--library-item-grid);
+ gap: var(--spacing-300);
+ align-items: center;
+ background: var(--s2-gray-50);
+ padding: var(--spacing-300);
+ border-radius: var(--s2-radius-100);
+ margin-bottom: var(--spacing-200);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+/* Item Keys/Names */
+.library-item-name {
+ font-weight: 500;
+}
+
+.library-item-key {
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.library-item-key.icon {
+ color: var(--s2-blue-900);
+}
+
+.library-item-key.placeholder {
+ color: var(--s2-gray-900);
+}
+
+/* Item Paths/Values */
+.library-item-path,
+.library-item-value {
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+ color: var(--s2-gray-600);
+}
+
+/* Section Header */
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: var(--spacing-500);
+ margin-bottom: var(--spacing-400);
+
+ & h2 {
+ margin-bottom: var(--spacing-200);
+ }
+
+ & .form-section-subtitle {
+ margin: 0;
+ }
+}
+
+/* Search Input */
+.library-search {
+ padding: var(--spacing-300) var(--spacing-400);
+ border: 1px solid var(--s2-gray-300);
+ border-radius: var(--s2-radius-100);
+ font-size: 14px;
+ min-width: 250px;
+
+ &:focus {
+ outline: none;
+ border-color: var(--s2-blue-600);
+ box-shadow: 0 0 0 3px var(--s2-blue-100);
+ }
+}
+
+/* Existing Items Section */
+.existing-items-list {
+ margin-bottom: var(--spacing-600);
+ padding-bottom: var(--spacing-600);
+ border-bottom: 2px solid var(--s2-gray-200);
+
+ & h3 {
+ margin-bottom: var(--spacing-400);
+ font-size: 16px;
+ }
+}
+
+/* Items List Container */
+.items-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-200);
+}
+
+/* Individual Item in List */
+.item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--spacing-400);
+ padding: var(--spacing-400);
+ border-bottom: 1px solid var(--s2-gray-200);
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background: var(--s2-gray-50);
+ }
+}
+
+.item-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-200);
+ min-width: 0;
+}
+
+.item-name {
+ font-weight: 600;
+ color: var(--s2-gray-900);
+}
+
+.item-path,
+.item-value {
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+ color: var(--s2-gray-600);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Item Actions */
+.item-actions {
+ display: flex;
+ gap: var(--spacing-200);
+ flex-shrink: 0;
+}
+
+.edit-item-btn,
+.remove-item-btn {
+ padding: var(--spacing-200) var(--spacing-300);
+ font-size: 13px;
+ border-radius: var(--s2-radius-100);
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.edit-item-btn {
+ background: var(--s2-blue-50);
+ color: var(--s2-blue-700);
+ border: 1px solid var(--s2-blue-200);
+
+ &:hover {
+ background: var(--s2-blue-100);
+ border-color: var(--s2-blue-300);
+ }
+}
+
+.remove-item-btn {
+ background: var(--s2-red-50);
+ color: var(--s2-red-700);
+ border: 1px solid var(--s2-red-200);
+
+ &:hover {
+ background: var(--s2-red-100);
+ border-color: var(--s2-red-300);
+ }
+}
+
+/* Add New Section */
+.add-new-section {
+ margin-top: var(--spacing-600);
+
+ & h3 {
+ margin-bottom: var(--spacing-400);
+ font-size: 16px;
+ }
+}
+
+/* Pending Changes */
+.pending-changes {
+ margin-top: var(--spacing-500);
+ padding: var(--spacing-400);
+ background: var(--s2-yellow-50);
+ border: 1px solid var(--s2-yellow-200);
+ border-radius: var(--s2-radius-200);
+
+ & h4 {
+ margin: 0 0 var(--spacing-300);
+ font-size: 14px;
+ color: var(--s2-yellow-900);
+ }
+}
+
+/* State Messages */
+.loading-message,
+.empty-state,
+.no-results {
+ padding: var(--spacing-500);
+ text-align: center;
+ color: var(--s2-gray-600);
+ font-size: 14px;
+}
+
+.empty-state {
+ background: var(--s2-gray-50);
+ border-radius: var(--s2-radius-200);
+ margin-bottom: var(--spacing-500);
+}
+
+.no-results {
+ padding: var(--spacing-600);
+ font-style: italic;
+}
+
+/* Secondary Button */
+button.secondary {
+ background: transparent;
+ color: var(--s2-gray-700);
+ border: 1px solid var(--s2-gray-300);
+
+ &:hover {
+ background: var(--s2-gray-50);
+ border-color: var(--s2-gray-400);
+ }
+}
+
+/* ========== Responsive ========== */
+
+@media (width <= 900px) {
+ .library-item-form,
+ .templates-form,
+ .icons-form,
+ .placeholders-form {
+ grid-template-columns: 1fr;
+ }
+
+ .library-item {
+ grid-template-columns: 1fr auto;
+ gap: var(--spacing-200);
+ }
+
+ .library-item-path,
+ .library-item-value {
+ grid-column: 1;
+ }
+
+ .section-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .library-search {
+ min-width: 0;
+ }
+
+ .item {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .item-actions {
+ justify-content: flex-end;
+ }
+}
diff --git a/tools/admin/styles/nav.css b/tools/admin/styles/nav.css
new file mode 100644
index 0000000..08772cb
--- /dev/null
+++ b/tools/admin/styles/nav.css
@@ -0,0 +1,66 @@
+.nav-section {
+ margin-bottom: var(--spacing-600);
+ padding-bottom: var(--spacing-500);
+ border-bottom: 1px solid var(--s2-gray-200);
+
+ &:last-child {
+ margin-bottom: 0;
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+}
+
+.nav-section-title {
+ margin: 0 0 var(--spacing-400);
+ padding: 0 var(--spacing-600);
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: var(--s2-gray-400);
+}
+
+.nav-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.nav-item {
+ margin: 0;
+}
+
+.nav-link {
+ display: block;
+ padding: var(--spacing-300) var(--spacing-600);
+ color: var(--s2-gray-700);
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s;
+ border-left: 3px solid transparent;
+ border-radius: 0;
+
+ &:hover {
+ background: var(--s2-gray-100);
+ color: var(--s2-gray-900);
+ }
+
+ &.active {
+ background: var(--s2-blue-50);
+ color: var(--s2-blue-700);
+ font-weight: 600;
+ border-left-color: var(--s2-blue-600);
+ }
+
+ &.disabled {
+ color: var(--s2-gray-400);
+ cursor: not-allowed;
+ opacity: 0.5;
+
+ &:hover {
+ background: transparent;
+ color: var(--s2-gray-400);
+ }
+ }
+}
diff --git a/tools/admin/styles/page-picker.css b/tools/admin/styles/page-picker.css
new file mode 100644
index 0000000..617d48c
--- /dev/null
+++ b/tools/admin/styles/page-picker.css
@@ -0,0 +1,148 @@
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgb(0 0 0 / 50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal {
+ background: white;
+ border-radius: var(--s2-radius-200);
+ padding: var(--spacing-600);
+ max-width: 800px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+ box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
+
+ & h2 {
+ margin-top: 0;
+ }
+}
+
+.page-list {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 2px solid var(--s2-gray-200);
+ border-radius: var(--s2-radius-100);
+ padding: var(--spacing-400);
+ margin: var(--spacing-400) 0;
+}
+
+.tree-view {
+ padding: 0;
+}
+
+.tree-item {
+ margin: 0;
+}
+
+.folder-item {
+ margin-bottom: var(--spacing-200);
+}
+
+.folder-toggle {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+ width: 100%;
+ padding: var(--spacing-200);
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ font-family: var(--body-font-family);
+ font-size: var(--s2-font-size-200);
+ text-align: left;
+ border-radius: var(--s2-radius-100);
+ transition: background 0.2s;
+
+ &:hover {
+ background: var(--s2-gray-50);
+ }
+}
+
+.folder-icon {
+ flex-shrink: 0;
+ font-size: 16px;
+}
+
+.folder-name {
+ flex: 1;
+ font-weight: 500;
+}
+
+.toggle-arrow {
+ flex-shrink: 0;
+ font-size: 12px;
+ transition: transform 0.2s;
+}
+
+.folder-contents {
+ margin-left: var(--spacing-300);
+
+ &.hidden {
+ display: none;
+ }
+}
+
+.folder-loading {
+ padding: var(--spacing-200) var(--spacing-300);
+ color: var(--s2-gray-600);
+ font-size: var(--s2-font-size-100);
+ font-style: italic;
+}
+
+.file-item {
+ margin-bottom: 2px;
+}
+
+.page-checkbox {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+ padding: var(--spacing-200);
+ cursor: pointer;
+ font-size: var(--s2-font-size-200);
+ transition: background 0.2s;
+ border-radius: var(--s2-radius-100);
+ width: 100%;
+
+ &:hover {
+ background: var(--s2-gray-50);
+ }
+
+ &.selected {
+ background: var(--s2-blue-200);
+ }
+
+ & input[type="checkbox"] {
+ cursor: pointer;
+ flex-shrink: 0;
+ }
+}
+
+.page-icon {
+ flex-shrink: 0;
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+
+ & img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+}
+
+.page-name {
+ flex: 1;
+ font-family: var(--mono-font-family);
+ font-size: 13px;
+}
diff --git a/tools/admin/styles/progress.css b/tools/admin/styles/progress.css
new file mode 100644
index 0000000..1d05592
--- /dev/null
+++ b/tools/admin/styles/progress.css
@@ -0,0 +1,150 @@
+.import-card {
+ border-radius: 18px;
+ padding: var(--spacing-400);
+ border: 2px dashed rgb(0 0 0 / 20%);
+ transition: all 0.2s;
+
+ &.errors {
+ transition: transform 0.2s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgb(0 0 0 / 10%);
+ }
+ }
+}
+
+.import-card-blue {
+ background: rgb(181 230 252);
+}
+
+.import-card-lime {
+ background: rgb(202 255 164);
+}
+
+.import-card-green {
+ background: rgb(215 247 225);
+}
+
+.import-card-cyan {
+ background: rgb(202 248 250);
+}
+
+.import-card-red {
+ background: rgb(255 214 209);
+}
+
+.import-card-header {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-200);
+ margin-bottom: var(--spacing-300);
+}
+
+.import-card h3 {
+ margin: 0;
+ font-size: var(--s2-font-size-300);
+ font-weight: 600;
+}
+
+.import-card-body p {
+ margin: 0 0 var(--spacing-200);
+ font-size: var(--s2-font-size-200);
+}
+
+.import-card-value {
+ font-size: var(--s2-font-size-700);
+ font-weight: 700;
+ line-height: 1;
+ margin: 0;
+}
+
+.repo-path {
+ overflow-wrap: break-word;
+ word-break: break-all;
+ font-size: var(--s2-font-size-200);
+}
+
+.import-card-link {
+ display: inline-block;
+ color: var(--s2-blue-700);
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ margin-top: var(--spacing-200);
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.status-cards-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-400);
+ margin: var(--spacing-500) 0;
+}
+
+.view-errors-link {
+ color: var(--s2-red-700) !important;
+ margin-top: var(--spacing-300);
+}
+
+.error-details {
+ margin-top: var(--spacing-300);
+ font-size: 13px;
+
+ & summary {
+ cursor: pointer;
+ color: var(--s2-red-700);
+ font-weight: 600;
+ }
+}
+
+.error-list {
+ margin: var(--spacing-200) 0 0;
+ padding-left: var(--spacing-400);
+ list-style: disc;
+
+ & li {
+ margin-bottom: var(--spacing-100);
+ color: var(--s2-red-700);
+ }
+}
+
+.preview-notice {
+ margin: var(--spacing-600) 0;
+ padding: var(--spacing-500);
+ background: var(--s2-blue-100);
+ border-radius: var(--s2-radius-200);
+ border: 2px solid var(--s2-blue-200);
+
+ & h3 {
+ margin: 0 0 var(--spacing-300);
+ color: var(--s2-blue-900);
+ }
+
+ & p {
+ margin: 0 0 var(--spacing-400);
+ line-height: 1.6;
+ }
+}
+
+.preview-notice-content {
+ max-width: 600px;
+}
+
+.preview-notice-link {
+ display: inline-block;
+ padding: var(--spacing-300) var(--spacing-500);
+ background: var(--s2-blue-600);
+ color: white;
+ text-decoration: none;
+ border-radius: var(--s2-radius-100);
+ font-weight: 500;
+ transition: background 0.2s;
+
+ &:hover {
+ background: var(--s2-blue-700);
+ }
+}
diff --git a/tools/library-setup/utils/block-analysis.js b/tools/admin/utils/block-analysis.js
similarity index 100%
rename from tools/library-setup/utils/block-analysis.js
rename to tools/admin/utils/block-analysis.js
diff --git a/tools/library-setup/utils/content-extract.js b/tools/admin/utils/content-extract.js
similarity index 100%
rename from tools/library-setup/utils/content-extract.js
rename to tools/admin/utils/content-extract.js
diff --git a/tools/admin/utils/css-loader.js b/tools/admin/utils/css-loader.js
new file mode 100644
index 0000000..a34cb39
--- /dev/null
+++ b/tools/admin/utils/css-loader.js
@@ -0,0 +1,22 @@
+const loadedStyles = new Set();
+
+export default async function loadCSS(filename) {
+ if (loadedStyles.has(filename)) {
+ return;
+ }
+
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = `styles/${filename}`;
+
+ const promise = new Promise((resolve, reject) => {
+ link.onload = () => {
+ loadedStyles.add(filename);
+ resolve();
+ };
+ link.onerror = reject;
+ });
+
+ document.head.appendChild(link);
+ await promise;
+}
diff --git a/tools/admin/utils/da-api.js b/tools/admin/utils/da-api.js
new file mode 100644
index 0000000..cc85563
--- /dev/null
+++ b/tools/admin/utils/da-api.js
@@ -0,0 +1,1142 @@
+import state from '../app/state.js';
+import { LIBRARY_BLOCKS_PATH, CONTENT_DA_LIVE_BASE } from '../config.js';
+
+const DA_ADMIN = 'https://admin.da.live';
+
+// Library item order priority (Blocks must always be first)
+const LIBRARY_ORDER = {
+ Blocks: 0,
+ Templates: 1,
+ Icons: 2,
+ Placeholders: 3,
+};
+
+function sortLibraryData(libraryData) {
+ return libraryData.sort((a, b) => {
+ const orderA = LIBRARY_ORDER[a.title] ?? 999;
+ const orderB = LIBRARY_ORDER[b.title] ?? 999;
+ return orderA - orderB;
+ });
+}
+
+async function daFetch(url, options = {}) {
+ const token = state.daToken;
+
+ const headers = {
+ ...options.headers,
+ };
+
+ if (token) {
+ headers.Authorization = `Bearer ${token}`;
+ }
+
+ const response = await fetch(url, { ...options, headers });
+
+ return response;
+}
+
+export async function fetchSiteConfig(org, site) {
+ const url = `${DA_ADMIN}/config/${org}/${site}`;
+
+ try {
+ const response = await daFetch(url);
+
+ if (response.status === 404) {
+ return null;
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch config.json: ${response.status}`);
+ }
+
+ return response.json();
+ } catch (error) {
+ return null;
+ }
+}
+
+async function checkBlockDocExists(org, site, blockName) {
+ const path = `/${org}/${site}/library/blocks/${blockName}`;
+ const url = `${DA_ADMIN}/source${path}.html`;
+
+ try {
+ const response = await daFetch(url, { method: 'HEAD' });
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+}
+
+async function createBlockDocVersion(org, site, blockName) {
+ const path = `/${org}/${site}/library/blocks/${blockName}`;
+ const url = `${DA_ADMIN}/versionsource${path}.html`;
+
+ try {
+ const response = await daFetch(url, { method: 'POST' });
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+}
+
+export async function uploadBlockDoc(org, site, blockName, htmlContent) {
+ const path = `/${org}/${site}/library/blocks/${blockName}`;
+ const url = `${DA_ADMIN}/source${path}.html`;
+
+ try {
+ const exists = await checkBlockDocExists(org, site, blockName);
+ if (exists) {
+ await createBlockDocVersion(org, site, blockName);
+ }
+
+ const formData = new FormData();
+ const blob = new Blob([htmlContent], { type: 'text/html' });
+ formData.set('data', blob);
+
+ const response = await daFetch(url, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
+ }
+
+ return {
+ success: true,
+ path,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ path,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchBlocksJSON(org, site) {
+ let path = `${org}/${site}/library/blocks.json`;
+
+ const config = await fetchSiteConfig(org, site);
+ if (config?.library?.data) {
+ const blocksEntry = config.library.data.find((entry) => entry.title === 'Blocks');
+ if (blocksEntry?.path) {
+ path = blocksEntry.path.replace('https://content.da.live/', '');
+ }
+ }
+
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const response = await daFetch(url);
+
+ if (response.status === 404) {
+ return null;
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch blocks.json: ${response.status}`);
+ }
+
+ return response.json();
+ } catch (error) {
+ return null;
+ }
+}
+
+export async function updateBlocksJSON(org, site, config) {
+ let path = `${org}/${site}/library/blocks.json`;
+
+ const siteConfig = await fetchSiteConfig(org, site);
+ if (siteConfig?.library?.data) {
+ const blocksEntry = siteConfig.library.data.find((entry) => entry.title === 'Blocks');
+ if (blocksEntry?.path) {
+ path = blocksEntry.path.replace('https://content.da.live/', '');
+ }
+ }
+
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const formData = new FormData();
+ const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
+ formData.set('data', blob);
+
+ const response = await daFetch(url, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update blocks.json: ${response.status}`);
+ }
+
+ return {
+ success: true,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function batchUploadBlocks(org, site, blocks, onProgress, batchSize = 5) {
+ const results = [];
+ let processed = 0;
+
+ const batches = [];
+ for (let i = 0; i < blocks.length; i += batchSize) {
+ batches.push({
+ blocks: blocks.slice(i, i + batchSize),
+ startIndex: i,
+ });
+ }
+
+ await batches.reduce(async (previousPromise, { blocks: batch, startIndex }) => {
+ await previousPromise;
+
+ const processBatch = async (block, batchIndex) => {
+ const currentIndex = startIndex + batchIndex;
+ if (onProgress) {
+ onProgress({
+ current: currentIndex + 1,
+ total: blocks.length,
+ blockName: block.name,
+ status: 'uploading',
+ });
+ }
+
+ const result = await uploadBlockDoc(
+ org,
+ site,
+ block.name,
+ block.html,
+ );
+ processed += 1;
+
+ if (onProgress) {
+ onProgress({
+ current: processed,
+ total: blocks.length,
+ blockName: block.name,
+ status: result.success ? 'success' : 'error',
+ });
+ }
+
+ return {
+ name: block.name,
+ ...result,
+ };
+ };
+
+ const batchResults = await Promise.all(batch.map(processBatch));
+ results.push(...batchResults);
+ }, Promise.resolve());
+
+ return results;
+}
+
+export async function validateSite(org, site) {
+ const url = `https://admin.hlx.page/config/${org}/sites/${site}.json`;
+
+ try {
+ const response = await daFetch(url, { method: 'HEAD' });
+ return response.ok;
+ } catch (error) {
+ return false;
+ }
+}
+
+export async function updateSiteConfig(org, site, config) {
+ const url = `${DA_ADMIN}/config/${org}/${site}`;
+
+ try {
+ const formData = new FormData();
+ formData.append('config', JSON.stringify(config));
+
+ const response = await daFetch(url, {
+ method: 'PUT',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update config.json: ${response.status}`);
+ }
+
+ return {
+ success: true,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function registerLibrary(org, site) {
+ try {
+ let config = await fetchSiteConfig(org, site);
+ const blocksPath = `${CONTENT_DA_LIVE_BASE}/${org}/${site}/${LIBRARY_BLOCKS_PATH}.json`;
+ let wasCreated = false;
+
+ if (!config) {
+ config = {
+ ':version': 3,
+ ':names': ['library'],
+ ':type': 'multi-sheet',
+ library: {
+ total: 1,
+ limit: 1,
+ offset: 0,
+ data: [
+ {
+ title: 'Blocks',
+ path: blocksPath,
+ },
+ ],
+ },
+ };
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ created: true,
+ error: result.error,
+ };
+ }
+
+ const configType = config[':type'];
+
+ if (configType === 'sheet') {
+ const existingData = config.data || [];
+ const existingColWidths = config[':colWidths'];
+
+ config = {
+ ':version': 3,
+ ':names': ['data', 'library'],
+ ':type': 'multi-sheet',
+ data: {
+ total: existingData.length,
+ limit: existingData.length,
+ offset: 0,
+ data: existingData,
+ },
+ library: {
+ total: 1,
+ limit: 1,
+ offset: 0,
+ data: [
+ {
+ title: 'Blocks',
+ path: blocksPath,
+ },
+ ],
+ },
+ };
+
+ if (existingColWidths) {
+ config.data[':colWidths'] = existingColWidths;
+ }
+
+ wasCreated = true;
+ } else if (configType === 'multi-sheet') {
+ if (!config.library) {
+ if (!config[':names'].includes('library')) {
+ config[':names'].push('library');
+ }
+
+ config.library = {
+ total: 1,
+ limit: 1,
+ offset: 0,
+ data: [
+ {
+ title: 'Blocks',
+ path: blocksPath,
+ },
+ ],
+ };
+
+ wasCreated = true;
+ } else {
+ const libraryData = config.library.data || [];
+ const blocksIndex = libraryData.findIndex((entry) => entry.title === 'Blocks');
+
+ if (blocksIndex === -1) {
+ libraryData.push({
+ title: 'Blocks',
+ path: blocksPath,
+ });
+
+ config.library.data = libraryData;
+ config.library.total = libraryData.length;
+ config.library.limit = libraryData.length;
+ wasCreated = true;
+ } else {
+ const existingPath = libraryData[blocksIndex].path;
+ if (existingPath === blocksPath) {
+ return {
+ success: true,
+ created: false,
+ error: null,
+ };
+ }
+
+ libraryData[blocksIndex].path = blocksPath;
+ config.library.data = libraryData;
+ }
+ }
+ } else {
+ config = {
+ ':version': 3,
+ ':names': ['library'],
+ ':type': 'multi-sheet',
+ library: {
+ total: 1,
+ limit: 1,
+ offset: 0,
+ data: [
+ {
+ title: 'Blocks',
+ path: blocksPath,
+ },
+ ],
+ },
+ };
+
+ wasCreated = true;
+ }
+
+ if (!config[':version']) {
+ config[':version'] = 3;
+ }
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ created: wasCreated,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ created: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function registerTemplatesInConfig(org, site) {
+ try {
+ const config = await fetchSiteConfig(org, site);
+ const templatesPath = `${CONTENT_DA_LIVE_BASE}/${org}/${site}/library/templates.json`;
+
+ if (!config || !config.library) {
+ return {
+ success: false,
+ error: 'Site config not initialized. Run Blocks setup first.',
+ };
+ }
+
+ const libraryData = config.library.data || [];
+ const templatesIndex = libraryData.findIndex((entry) => entry.title === 'Templates');
+
+ if (templatesIndex === -1) {
+ libraryData.push({
+ title: 'Templates',
+ path: templatesPath,
+ });
+ } else {
+ libraryData[templatesIndex].path = templatesPath;
+ }
+
+ // Ensure correct order (Blocks must be first)
+ const sortedData = sortLibraryData(libraryData);
+ config.library.data = sortedData;
+ config.library.total = sortedData.length;
+ config.library.limit = sortedData.length;
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function uploadTemplateDoc(org, site, templateName, sourcePath) {
+ const sourceUrl = `${DA_ADMIN}/source/${org}/${site}${sourcePath}.html`;
+ const targetPath = `/${org}/${site}/library/templates/${templateName}`;
+ const targetUrl = `${DA_ADMIN}/source${targetPath}.html`;
+
+ try {
+ const sourceResponse = await daFetch(sourceUrl);
+ if (!sourceResponse.ok) {
+ throw new Error(`Failed to fetch source page: ${sourceResponse.status}`);
+ }
+
+ const htmlContent = await sourceResponse.text();
+
+ const formData = new FormData();
+ const blob = new Blob([htmlContent], { type: 'text/html' });
+ formData.set('data', blob);
+
+ const targetResponse = await daFetch(targetUrl, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!targetResponse.ok) {
+ throw new Error(`Upload failed: ${targetResponse.status}`);
+ }
+
+ return {
+ success: true,
+ path: targetPath,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ path: targetPath,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchTemplatesJSON(org, site) {
+ const path = `${org}/${site}/library/templates.json`;
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const response = await daFetch(url);
+
+ if (response.status === 404) {
+ return null;
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch templates.json: ${response.status}`);
+ }
+
+ return response.json();
+ } catch (error) {
+ return null;
+ }
+}
+
+export async function updateTemplatesJSON(org, site, config) {
+ const path = `${org}/${site}/library/templates.json`;
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const formData = new FormData();
+ const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
+ formData.set('data', blob);
+
+ const response = await daFetch(url, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update templates.json: ${response.status}`);
+ }
+
+ return {
+ success: true,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchIconsJSON(org, site) {
+ const path = `${org}/${site}/library/icons.json`;
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const response = await daFetch(url);
+
+ if (response.status === 404) {
+ return null;
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch icons.json: ${response.status}`);
+ }
+
+ return response.json();
+ } catch (error) {
+ return null;
+ }
+}
+
+export async function updateIconsJSON(org, site, config) {
+ const path = `${org}/${site}/library/icons.json`;
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const formData = new FormData();
+ const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
+ formData.set('data', blob);
+
+ const response = await daFetch(url, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update icons.json: ${response.status}`);
+ }
+
+ return {
+ success: true,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function uploadIconDoc(org, site, iconName, sourcePath) {
+ const sourceUrl = `${DA_ADMIN}/source/${org}/${site}${sourcePath}`;
+ const targetPath = `/${org}/${site}/library/icons/${iconName}`;
+ const targetUrl = `${DA_ADMIN}/source${targetPath}.svg`;
+
+ try {
+ const sourceResponse = await daFetch(sourceUrl);
+ if (!sourceResponse.ok) {
+ throw new Error(`Failed to fetch source icon: ${sourceResponse.status}`);
+ }
+
+ const svgContent = await sourceResponse.text();
+
+ const formData = new FormData();
+ const blob = new Blob([svgContent], { type: 'image/svg+xml' });
+ formData.set('data', blob);
+
+ const targetResponse = await daFetch(targetUrl, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!targetResponse.ok) {
+ throw new Error(`Upload failed: ${targetResponse.status}`);
+ }
+
+ return {
+ success: true,
+ path: targetPath,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ path: targetPath,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchPlaceholdersJSON(org, site) {
+ const path = `${org}/${site}/placeholders.json`;
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const response = await daFetch(url);
+
+ if (response.status === 404) {
+ return null;
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch placeholders.json: ${response.status}`);
+ }
+
+ return response.json();
+ } catch (error) {
+ return null;
+ }
+}
+
+export async function updatePlaceholdersJSON(org, site, config) {
+ const path = `${org}/${site}/placeholders.json`;
+ const url = `${DA_ADMIN}/source/${path}`;
+
+ try {
+ const formData = new FormData();
+ const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
+ formData.set('data', blob);
+
+ const response = await daFetch(url, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update placeholders.json: ${response.status}`);
+ }
+
+ return {
+ success: true,
+ error: null,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function registerIconsInConfig(org, site) {
+ try {
+ const config = await fetchSiteConfig(org, site);
+ const iconsPath = `${CONTENT_DA_LIVE_BASE}/${org}/${site}/library/icons.json`;
+
+ if (!config || !config.library) {
+ return {
+ success: false,
+ error: 'Site config not initialized. Run Blocks setup first.',
+ };
+ }
+
+ const libraryData = config.library.data || [];
+ const iconsIndex = libraryData.findIndex((entry) => entry.title === 'Icons');
+
+ if (iconsIndex === -1) {
+ libraryData.push({
+ title: 'Icons',
+ path: iconsPath,
+ format: '::',
+ });
+ } else {
+ libraryData[iconsIndex].path = iconsPath;
+ libraryData[iconsIndex].format = '::';
+ }
+
+ // Ensure correct order (Blocks must be first)
+ const sortedData = sortLibraryData(libraryData);
+ config.library.data = sortedData;
+ config.library.total = sortedData.length;
+ config.library.limit = sortedData.length;
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function registerPlaceholdersInConfig(org, site) {
+ try {
+ const config = await fetchSiteConfig(org, site);
+ const placeholdersPath = `${CONTENT_DA_LIVE_BASE}/${org}/${site}/placeholders.json`;
+
+ if (!config || !config.library) {
+ return {
+ success: false,
+ error: 'Site config not initialized. Run Blocks setup first.',
+ };
+ }
+
+ const libraryData = config.library.data || [];
+ const placeholdersIndex = libraryData.findIndex((entry) => entry.title === 'Placeholders');
+
+ if (placeholdersIndex === -1) {
+ libraryData.push({
+ title: 'Placeholders',
+ path: placeholdersPath,
+ format: '{{}}',
+ });
+ } else {
+ libraryData[placeholdersIndex].path = placeholdersPath;
+ libraryData[placeholdersIndex].format = '{{}}';
+ }
+
+ // Ensure correct order (Blocks must be first)
+ const sortedData = sortLibraryData(libraryData);
+ config.library.data = sortedData;
+ config.library.total = sortedData.length;
+ config.library.limit = sortedData.length;
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchAemAssetsConfig(org, site) {
+ try {
+ const config = await fetchSiteConfig(org, site);
+ if (!config || !config.data || !config.data.data) {
+ return {
+ repositoryId: '',
+ prodOrigin: '',
+ imageType: false,
+ renditionsSelect: false,
+ dmDelivery: false,
+ smartCropSelect: false,
+ };
+ }
+
+ const dataItems = config.data.data;
+ const configMap = new Map(dataItems.map((item) => [item.key, item.value]));
+
+ return {
+ repositoryId: configMap.get('aem.repositoryId') || '',
+ prodOrigin: configMap.get('aem.assets.prod.origin') || '',
+ imageType: configMap.get('aem.assets.image.type') === 'link',
+ renditionsSelect: configMap.get('aem.assets.renditions.select') === 'on',
+ dmDelivery: configMap.get('aem.asset.dm.delivery') === 'on',
+ smartCropSelect: configMap.get('aem.asset.smartcrop.select') === 'on',
+ };
+ } catch (error) {
+ return {
+ repositoryId: '',
+ prodOrigin: '',
+ imageType: false,
+ renditionsSelect: false,
+ dmDelivery: false,
+ smartCropSelect: false,
+ };
+ }
+}
+
+export async function updateAemAssetsConfig(org, site, aemConfig) {
+ try {
+ let config = await fetchSiteConfig(org, site);
+
+ if (!config) {
+ config = {
+ ':version': 3,
+ ':type': 'multi-sheet',
+ ':names': ['data'],
+ data: {
+ total: 0,
+ limit: 0,
+ offset: 0,
+ data: [],
+ },
+ };
+ }
+
+ if (!config.data) {
+ config.data = {
+ total: 0,
+ limit: 0,
+ offset: 0,
+ data: [],
+ };
+ }
+
+ if (!config.data.data) {
+ config.data.data = [];
+ }
+
+ const dataItems = [...config.data.data];
+ const aemKeys = [
+ 'aem.repositoryId',
+ 'aem.assets.prod.origin',
+ 'aem.assets.image.type',
+ 'aem.assets.renditions.select',
+ 'aem.asset.dm.delivery',
+ 'aem.asset.smartcrop.select',
+ ];
+
+ const filteredData = dataItems.filter((item) => !aemKeys.includes(item.key));
+
+ if (aemConfig.repositoryId) {
+ filteredData.push({
+ key: 'aem.repositoryId',
+ value: aemConfig.repositoryId,
+ });
+ }
+
+ if (aemConfig.prodOrigin) {
+ filteredData.push({
+ key: 'aem.assets.prod.origin',
+ value: aemConfig.prodOrigin,
+ });
+ }
+
+ if (aemConfig.imageType) {
+ filteredData.push({
+ key: 'aem.assets.image.type',
+ value: 'link',
+ });
+ }
+
+ if (aemConfig.renditionsSelect) {
+ filteredData.push({
+ key: 'aem.assets.renditions.select',
+ value: 'on',
+ });
+ }
+
+ if (aemConfig.dmDelivery) {
+ filteredData.push({
+ key: 'aem.asset.dm.delivery',
+ value: 'on',
+ });
+ }
+
+ if (aemConfig.smartCropSelect) {
+ filteredData.push({
+ key: 'aem.asset.smartcrop.select',
+ value: 'on',
+ });
+ }
+
+ config.data.data = filteredData;
+ config.data.total = filteredData.length;
+ config.data.limit = filteredData.length;
+
+ if (!config[':names']) {
+ config[':names'] = ['data'];
+ }
+ if (!config[':names'].includes('data')) {
+ config[':names'].push('data');
+ }
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchTranslationConfig(org, site) {
+ try {
+ const config = await fetchSiteConfig(org, site);
+ if (!config || !config.data || !config.data.data) {
+ return {
+ translateBehavior: 'overwrite',
+ translateStaging: 'off',
+ rolloutBehavior: 'overwrite',
+ };
+ }
+
+ const dataItems = config.data.data;
+ const configMap = new Map(dataItems.map((item) => [item.key, item.value]));
+
+ return {
+ translateBehavior: configMap.get('translate.behavior') || 'overwrite',
+ translateStaging: configMap.get('translate.staging') || 'off',
+ rolloutBehavior: configMap.get('rollout.behavior') || 'overwrite',
+ };
+ } catch (error) {
+ return {
+ translateBehavior: 'overwrite',
+ translateStaging: 'off',
+ rolloutBehavior: 'overwrite',
+ };
+ }
+}
+
+export async function updateTranslationConfig(org, site, translationConfig) {
+ try {
+ let config = await fetchSiteConfig(org, site);
+
+ if (!config) {
+ config = {
+ ':version': 3,
+ ':type': 'multi-sheet',
+ ':names': ['data'],
+ data: {
+ total: 0,
+ limit: 0,
+ offset: 0,
+ data: [],
+ },
+ };
+ }
+
+ if (!config.data) {
+ config.data = {
+ total: 0,
+ limit: 0,
+ offset: 0,
+ data: [],
+ };
+ }
+
+ if (!config.data.data) {
+ config.data.data = [];
+ }
+
+ const dataItems = [...config.data.data];
+ const translationKeys = [
+ 'translate.behavior',
+ 'translate.staging',
+ 'rollout.behavior',
+ ];
+
+ const filteredData = dataItems.filter((item) => !translationKeys.includes(item.key));
+
+ filteredData.push({
+ key: 'translate.behavior',
+ value: translationConfig.translateBehavior,
+ });
+
+ filteredData.push({
+ key: 'translate.staging',
+ value: translationConfig.translateStaging,
+ });
+
+ filteredData.push({
+ key: 'rollout.behavior',
+ value: translationConfig.rolloutBehavior,
+ });
+
+ config.data.data = filteredData;
+ config.data.total = filteredData.length;
+ config.data.limit = filteredData.length;
+
+ if (!config[':names']) {
+ config[':names'] = ['data'];
+ }
+ if (!config[':names'].includes('data')) {
+ config[':names'].push('data');
+ }
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
+
+export async function fetchUniversalEditorConfig(org, site) {
+ try {
+ const config = await fetchSiteConfig(org, site);
+ if (!config || !config.data || !config.data.data) {
+ return {
+ editorPath: '',
+ };
+ }
+
+ const dataItems = config.data.data;
+ const configMap = new Map(dataItems.map((item) => [item.key, item.value]));
+
+ return {
+ editorPath: configMap.get('editor.path') || '',
+ };
+ } catch (error) {
+ return {
+ editorPath: '',
+ };
+ }
+}
+
+export async function updateUniversalEditorConfig(org, site, ueConfig) {
+ try {
+ let config = await fetchSiteConfig(org, site);
+
+ if (!config) {
+ config = {
+ ':version': 3,
+ ':type': 'multi-sheet',
+ ':names': ['data'],
+ data: {
+ total: 0,
+ limit: 0,
+ offset: 0,
+ data: [],
+ },
+ };
+ }
+
+ if (!config.data) {
+ config.data = {
+ total: 0,
+ limit: 0,
+ offset: 0,
+ data: [],
+ };
+ }
+
+ if (!config.data.data) {
+ config.data.data = [];
+ }
+
+ const dataItems = [...config.data.data];
+ const ueKeys = ['editor.path'];
+
+ const filteredData = dataItems.filter((item) => !ueKeys.includes(item.key));
+
+ if (ueConfig.editorPath) {
+ filteredData.push({
+ key: 'editor.path',
+ value: ueConfig.editorPath,
+ });
+ }
+
+ config.data.data = filteredData;
+ config.data.total = filteredData.length;
+ config.data.limit = filteredData.length;
+
+ if (!config[':names']) {
+ config[':names'] = ['data'];
+ }
+ if (!config[':names'].includes('data')) {
+ config[':names'].push('data');
+ }
+
+ const result = await updateSiteConfig(org, site, config);
+ return {
+ success: result.success,
+ error: result.error,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+}
diff --git a/tools/library-setup/utils/doc-generator.js b/tools/admin/utils/doc-generator.js
similarity index 100%
rename from tools/library-setup/utils/doc-generator.js
rename to tools/admin/utils/doc-generator.js
diff --git a/tools/library-setup/utils/github-api.js b/tools/admin/utils/github-api.js
similarity index 100%
rename from tools/library-setup/utils/github-api.js
rename to tools/admin/utils/github-api.js
diff --git a/tools/library-setup/utils/github-parser.js b/tools/admin/utils/github-parser.js
similarity index 100%
rename from tools/library-setup/utils/github-parser.js
rename to tools/admin/utils/github-parser.js
diff --git a/tools/library-setup/utils/token-storage.js b/tools/admin/utils/token-storage.js
similarity index 85%
rename from tools/library-setup/utils/token-storage.js
rename to tools/admin/utils/token-storage.js
index 571b307..2b1f084 100644
--- a/tools/library-setup/utils/token-storage.js
+++ b/tools/admin/utils/token-storage.js
@@ -1,4 +1,4 @@
-const STORAGE_KEY = 'library-setup-github-token';
+const STORAGE_KEY = 'siteadmin-github-token';
const TokenStorage = {
get() {
diff --git a/tools/library-setup/README.md b/tools/library-setup/README.md
deleted file mode 100644
index 4bc2234..0000000
--- a/tools/library-setup/README.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# Library Setup Tool
-
-Generate block documentation for your DA.live library from GitHub repositories.
-
-## Installation
-
-**Important:** Run these commands from your project root (where the `tools/` folder should be created or updated).
-
-### Option 1: Using npx (Recommended)
-
-```bash
-npx degit kmurugulla/brightpath/tools/library-setup tools/library-setup
-```
-
-### Option 2: Using curl
-
-```bash
-curl -L https://github.com/kmurugulla/brightpath/archive/refs/heads/main.tar.gz | \
- tar -xz --strip=3 "brightpath-main/tools/library-setup" && \
- mv library-setup tools/
-```
-
-## Getting Started
-
-- Access via: `https://da.live/app/{org}/{site}/tools/library-setup/library-setup?ref=local`
-- `ref=local` points to your local development server
-- Customize the code as needed for your project
-
-## Setup Mode
-
-
-
-Use this mode to create a new block library from scratch:
-
-- Enter your GitHub repository URL to discover blocks
-- Private repositories supported - enter a GitHub token when prompted
-- Select which blocks to include in your library
-- New blocks detected automatically - visual indicators show blocks not yet in blocks.json
-- Use "Select New Only" button to quickly select just the newly discovered blocks
-- Blocks are automatically analyzed for structure, variants, and features
-- Placeholder documentation generated based on block code analysis
-- Optionally select sample pages to extract real content examples
-- Automatically creates version snapshots of existing block docs before overwriting
-- Creates library structure in DA.live with blocks.json configuration
-- Updates site configuration to register the library
-
-## Refresh Documentation Mode
-
-
-
-Use this mode to update existing block documentation with new content:
-
-- No GitHub repository required - reads from existing library
-- Enter your DA.live organization and site name
-- Select which blocks to update
-- Add new sample pages to extract fresh content examples
-- Automatically creates version snapshots before updating any block documentation
-- Only updates blocks found in the selected pages
-- Preserves existing blocks not being updated
-- Maintains all library configuration settings
-
-## Requirements
-
-- Must be run from within DA.live for authentication
-- Write access to CONFIG for your organization (required to update site configuration) - [See permissions guide](https://docs.da.live/administrators/guides/permissions)
-- GitHub token needed only for private repositories (can be saved for future use)
-- Sample pages optional - tool generates intelligent placeholders without them
diff --git a/tools/library-setup/app/main.js b/tools/library-setup/app/main.js
deleted file mode 100644
index ea377be..0000000
--- a/tools/library-setup/app/main.js
+++ /dev/null
@@ -1,870 +0,0 @@
-/* eslint-disable import/no-absolute-path */
-
-/* eslint-disable import/no-unresolved */
-import DA_SDK from 'https://da.live/nx/utils/sdk.js';
-
-import state from './state.js';
-import * as templates from './templates.js';
-import * as githubOps from '../operations/github.js';
-import * as libraryOps from '../operations/library.js';
-import * as pagesOps from '../operations/pages.js';
-import * as daApi from '../utils/da-api.js';
-import TokenStorage from '../utils/token-storage.js';
-import GitHubAPI from '../utils/github-api.js';
-
-const app = {
- async init() {
- const container = document.getElementById('app-container');
- if (!container) {
- throw new Error('App container not found');
- }
-
- try {
- const { context, token } = await DA_SDK;
- state.daToken = token;
-
- if (context?.org) {
- state.org = context.org;
- }
-
- if (context?.repo) {
- state.site = context.repo;
- }
- } catch (error) {
- const urlMatch = window.location.pathname.match(/^\/app\/([^/]+)\/([^/]+)/);
- if (urlMatch) {
- const [, org, site] = urlMatch;
- state.org = org;
- state.site = site;
- }
- }
-
- if (TokenStorage.exists()) {
- state.githubToken = TokenStorage.get();
- }
-
- this.container = container;
- this.render();
- this.attachEventListeners();
-
- if (state.mode === 'refresh' && state.org && state.site) {
- await this.handleLoadExistingBlocks();
- }
- },
-
- render() {
- const sections = [];
-
- sections.push(templates.modeToggleTemplate({
- currentMode: state.mode,
- }));
-
- if (state.mode === 'setup') {
- sections.push(templates.githubSectionTemplate({
- isValidated: state.repositoryValidated,
- validating: state.validating,
- githubUrl: state.githubUrl,
- message: state.errors.github ? templates.messageTemplate(state.errors.github, 'error') : '',
- }));
-
- if (state.needsToken && !state.repositoryValidated) {
- sections.push(templates.tokenInputTemplate({
- hasSavedToken: TokenStorage.exists(),
- }));
- }
- }
-
- if ((state.mode === 'setup' && state.repositoryValidated) || state.mode === 'refresh') {
- sections.push(templates.siteSectionTemplate({
- org: state.org,
- site: state.site,
- message: state.errors.site ? templates.messageTemplate(state.errors.site, 'error') : '',
- mode: state.mode,
- }));
- }
-
- if (state.blocksDiscovered) {
- sections.push(templates.blocksListTemplate({
- blocks: state.blocks,
- selectedBlocks: state.selectedBlocks,
- message: state.errors.blocks ? templates.messageTemplate(state.errors.blocks, 'error') : '',
- }));
- }
-
- if (state.selectedBlocks.size > 0) {
- sections.push(templates.pagesSelectionTemplate({
- allSites: this.getAllSites(),
- pageSelections: state.pageSelections,
- message: state.errors.pages ? templates.messageTemplate(state.errors.pages, 'error') : '',
- daToken: state.daToken,
- org: state.org,
- mode: state.mode,
- }));
- }
-
- if (state.selectedBlocks.size > 0 && (state.processing || state.processResults.length > 0)) {
- sections.push(state.processing
- ? templates.processingTemplate({ processStatus: state.processStatus })
- : templates.finalStatusTemplate({
- processStatus: state.processStatus,
- org: state.org,
- repo: state.repo,
- }));
- } else if (state.selectedBlocks.size > 0) {
- sections.push(templates.initialStatusTemplate({
- org: state.org,
- repo: state.repo,
- blocksCount: state.blocks.length,
- mode: state.mode,
- libraryExists: state.libraryExists,
- }));
- }
-
- const content = sections.join('');
- const errorModal = templates.errorModalTemplate(
- state.processStatus.errors.messages,
- );
- this.container.innerHTML = templates.appTemplate(content) + errorModal;
- this.attachEventListeners();
- },
-
- attachEventListeners() {
- document.querySelectorAll('.mode-btn').forEach((btn) => {
- btn.addEventListener('click', (e) => {
- const newMode = e.target.dataset.mode;
- if (newMode !== state.mode) {
- this.handleModeChange(newMode);
- }
- });
- });
-
- const githubUrlInput = document.getElementById('github-url');
- if (githubUrlInput && !state.repositoryValidated) {
- githubUrlInput.addEventListener('input', (e) => {
- state.githubUrl = e.target.value;
- state.errors.github = '';
- });
-
- githubUrlInput.addEventListener('blur', (e) => this.handleGitHubUrlChange(e.target.value));
-
- githubUrlInput.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- this.handleGitHubUrlChange(e.target.value);
- }
- });
- }
-
- const orgInput = document.getElementById('org-name');
- if (orgInput) {
- orgInput.addEventListener('input', (e) => {
- state.org = e.target.value.trim();
- state.errors.site = '';
- });
- }
-
- const siteInput = document.getElementById('site-name');
- if (siteInput) {
- siteInput.addEventListener('input', (e) => {
- state.site = e.target.value.trim();
- state.errors.site = '';
- });
- }
-
- const loadExistingBlocksBtn = document.getElementById('load-existing-blocks');
- if (loadExistingBlocksBtn) {
- loadExistingBlocksBtn.addEventListener('click', () => this.handleLoadExistingBlocks());
- }
-
- const validateWithTokenBtn = document.getElementById('validate-with-token');
- if (validateWithTokenBtn) {
- validateWithTokenBtn.addEventListener('click', () => this.handleValidateWithToken());
- }
-
- const clearTokenBtn = document.getElementById('clear-token');
- if (clearTokenBtn) {
- clearTokenBtn.addEventListener('click', () => this.handleClearToken());
- }
-
- const startBtn = document.getElementById('start-processing');
- if (startBtn) {
- startBtn.addEventListener('click', () => this.handleStartProcessing());
- }
-
- document.querySelectorAll('[data-block-name]').forEach((checkbox) => {
- checkbox.addEventListener('change', (e) => {
- const { target } = e;
- const { blockName } = target.dataset;
- if (target.checked) {
- state.selectedBlocks.add(blockName);
- } else {
- state.selectedBlocks.delete(blockName);
- }
- this.render();
- });
- });
-
- const toggleAllBtn = document.getElementById('toggle-all-blocks');
- if (toggleAllBtn) {
- toggleAllBtn.addEventListener('click', () => this.toggleAllBlocks());
- }
-
- const selectNewOnlyBtn = document.getElementById('select-new-only');
- if (selectNewOnlyBtn) {
- selectNewOnlyBtn.addEventListener('click', () => this.selectNewBlocksOnly());
- }
-
- document.querySelectorAll('.select-pages-btn').forEach((btn) => {
- btn.addEventListener('click', (e) => {
- const { site } = e.target.dataset;
- this.openPagePicker(site);
- });
- });
-
- document.querySelectorAll('.remove-page-btn').forEach((btn) => {
- btn.addEventListener('click', (e) => {
- const { site, path } = e.target.dataset;
- this.removePage(site, path);
- });
- });
-
- document.querySelectorAll('.error-modal-close').forEach((btn) => {
- btn.addEventListener('click', () => this.hideErrorModal());
- });
-
- const errorModalOverlay = document.getElementById('error-modal');
- if (errorModalOverlay) {
- errorModalOverlay.addEventListener('click', (e) => {
- if (e.target === errorModalOverlay) {
- this.hideErrorModal();
- }
- });
- }
-
- const errorsCard = document.querySelector('.import-card.errors');
- if (errorsCard && state.processStatus.errors.count > 0) {
- errorsCard.style.cursor = 'pointer';
- errorsCard.addEventListener('click', () => this.showErrorModal());
- }
- },
-
- async handleModeChange(newMode) {
- state.mode = newMode;
- state.message = '';
- state.messageType = 'info';
- state.errors = {
- github: '', site: '', blocks: '', pages: '',
- };
-
- if (newMode === 'refresh') {
- state.repositoryValidated = false;
- state.blocksDiscovered = false;
- state.needsToken = false;
- state.githubUrl = '';
- state.blocks = [];
- state.selectedBlocks.clear();
- state.processing = false;
- state.processResults = [];
- state.validating = false;
- state.discovering = false;
- state.pageSelections = {};
-
- this.render();
- if (state.org && state.site) {
- await this.handleLoadExistingBlocks();
- }
- return;
- }
-
- if (newMode === 'setup') {
- state.repositoryValidated = false;
- state.blocksDiscovered = false;
- state.needsToken = false;
- state.githubUrl = '';
- state.blocks = [];
- state.selectedBlocks.clear();
- state.processing = false;
- state.processResults = [];
- state.validating = false;
- state.discovering = false;
- state.pageSelections = {};
- state.libraryExists = false;
- }
-
- this.render();
- },
-
- async handleGitHubUrlChange(url) {
- state.githubUrl = url.trim();
-
- const parsed = githubOps.parseGitHubURL(state.githubUrl);
- if (parsed && parsed.org && parsed.repo) {
- state.org = parsed.org;
- state.repo = parsed.repo;
- state.site = parsed.repo;
-
- await this.validateRepository();
- }
- },
-
- async validateRepository() {
- state.validating = true;
- this.render();
- try {
- const result = await githubOps.validateRepository(state.org, state.repo, state.githubToken);
-
- if (!result.valid) {
- if (result.error === 'rate_limit') {
- state.needsToken = true;
- state.errors.github = `GitHub API rate limit exceeded (resets at ${result.resetTime}). Please add a GitHub token to continue, or wait and try again.`;
- state.validating = false;
- this.render();
- return;
- }
-
- if (result.error === 'not_found') {
- state.errors.github = 'Repository not found. Please check the URL and try again.';
- state.validating = false;
- this.render();
- return;
- }
-
- if (result.error === 'private' && result.needsToken) {
- state.needsToken = true;
- state.errors.github = 'Unable to access repository. If this is a private repository, please enter a GitHub token below.';
- state.validating = false;
- this.render();
- return;
- }
-
- state.errors.github = result.error === 'private' ? 'Unable to access repository with provided token.' : result.error;
- state.validating = false;
- this.render();
- return;
- }
-
- state.repositoryValidated = true;
- state.needsToken = false;
-
- state.validating = false;
- this.render();
- await this.discoverBlocks();
- } catch (error) {
- let errorMsg = error.message;
- if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
- errorMsg = 'Please enter a valid GitHub repository URL.';
- } else if (errorMsg.includes('Failed to fetch') || errorMsg.includes('NetworkError')) {
- errorMsg = 'Network error. Please check your connection and try again.';
- }
-
- state.message = errorMsg;
- state.messageType = 'error';
- state.validating = false;
- this.render();
- }
- },
-
- async handleValidateWithToken() {
- const tokenInput = document.getElementById('github-token');
- const saveCheckbox = document.getElementById('save-token');
- const token = tokenInput?.value.trim();
-
- if (!token) {
- state.errors.github = 'Please enter a GitHub token';
- this.render();
- return;
- }
-
- if (saveCheckbox?.checked) {
- TokenStorage.set(token);
- }
-
- state.githubToken = token;
- await this.validateRepository();
- },
-
- handleClearToken() {
- TokenStorage.clear();
- state.githubToken = null;
- state.message = 'Saved token cleared';
- state.messageType = 'success';
- this.render();
- },
-
- async handleLoadExistingBlocks() {
- if (!state.org || !state.site) {
- state.errors.site = 'Please enter both organization and site name';
- this.render();
- return;
- }
-
- state.discovering = true;
- this.render();
-
- try {
- const blocks = await libraryOps.fetchExistingBlocks(state.org, state.site);
-
- if (blocks.length === 0) {
- state.errors.site = 'No library found at this location. Please run "Library Setup" first to create the library.';
- state.discovering = false;
- this.render();
- return;
- }
-
- state.blocks = blocks.map((block) => ({
- ...block,
- isNew: false,
- }));
- state.blocksDiscovered = true;
- state.discovering = false;
- state.selectedBlocks = new Set(blocks.map((b) => b.name));
- state.errors = {
- github: '', site: '', blocks: '', pages: '',
- };
- this.render();
- } catch (error) {
- state.errors.site = `Unable to load library: ${error.message}. Please run "Library Setup" first to create the library.`;
- state.discovering = false;
- this.render();
- }
- },
-
- async discoverBlocks() {
- state.discovering = true;
- this.render();
- try {
- const blocks = await githubOps.discoverBlocks(state.org, state.repo, state.githubToken);
-
- const existingBlocksJSON = await daApi.fetchBlocksJSON(state.org, state.site);
-
- const existingBlockNames = new Set(
- existingBlocksJSON?.data?.data?.map((b) => {
- const pathParts = b.path.split('/');
- return pathParts[pathParts.length - 1]; // Get last part of path (kebab-case name)
- }) || [],
- );
-
- state.blocks = blocks.map((block) => ({
- ...block,
- isNew: !existingBlockNames.has(block.name),
- }));
-
- state.blocksDiscovered = true;
- state.discovering = false;
-
- state.selectedBlocks = new Set(blocks.map((b) => b.name));
-
- const libraryCheck = await libraryOps.checkLibraryExists(state.org, state.site);
- state.libraryExists = libraryCheck.exists;
-
- this.render();
- } catch (error) {
- state.errors.github = `Block discovery failed: ${error.message}`;
- state.discovering = false;
- this.render();
- }
- },
-
- async handleStartProcessing() {
- if (!state.daToken) {
- state.errors.pages = 'DA.live authentication required. This tool must be run from within DA.live.';
- this.render();
- return;
- }
-
- const siteValid = await this.validateSite(state.org, state.site, state.daToken);
- if (!siteValid) {
- state.errors.site = `Site "${state.org}/${state.site}" not found in DA.live. Please verify the site name.`;
- this.render();
- return;
- }
-
- state.processing = true;
- const baseStatus = {
- github: { org: state.org, repo: state.repo, status: 'complete' },
- blocks: { total: state.selectedBlocks.size, status: 'complete' },
- blockDocs: { created: 0, total: state.selectedBlocks.size, status: 'pending' },
- errors: { count: 0, messages: [] },
- currentStep: state.mode === 'refresh' ? 'Starting documentation refresh...' : 'Starting library setup...',
- };
-
- if (state.mode === 'setup') {
- baseStatus.siteConfig = { status: 'pending', message: '' };
- baseStatus.blocksJson = { status: 'pending', message: '' };
- }
-
- state.processStatus = baseStatus;
- state.processResults = [];
- this.render();
-
- try {
- const selectedBlockNames = Array.from(state.selectedBlocks);
- const sitesWithPages = this.getAllSites().map((site) => ({
- org: state.org,
- site,
- pages: Array.from(state.pageSelections[site] || []),
- }));
-
- let githubApi = null;
- if (state.mode === 'setup' && state.org && state.repo) {
- githubApi = new GitHubAPI(state.org, state.repo, 'main', state.githubToken);
- }
-
- const results = await libraryOps.setupLibrary({
- org: state.org,
- site: state.site,
- blockNames: selectedBlockNames,
- sitesWithPages,
- onProgress: (progress) => this.handleProgress(progress),
- skipSiteConfig: state.mode === 'refresh',
- githubApi,
- });
-
- if (!results.success) {
- throw new Error(results.error || 'Library setup failed');
- }
-
- state.processStatus.currentStep = '';
- state.message = '';
- state.messageType = '';
- } catch (error) {
- state.processStatus.currentStep = `✗ Error: ${error.message}`;
- state.processStatus.errors.count += 1;
- state.processStatus.errors.messages.push({
- type: 'general',
- block: 'N/A',
- message: error.message,
- });
- state.message = `Processing failed: ${error.message}`;
- state.messageType = 'error';
- } finally {
- state.processing = false;
- this.render();
- }
- },
-
- handleProgress(progress) {
- if (progress.step === 'register') {
- if (state.processStatus.siteConfig) {
- if (progress.status === 'start') {
- state.processStatus.siteConfig.status = 'processing';
- state.processStatus.siteConfig.message = 'Registering library...';
- state.processStatus.currentStep = 'Configuring the Library...';
- } else if (progress.status === 'complete') {
- state.processStatus.siteConfig.status = 'complete';
- state.processStatus.siteConfig.message = 'Updated Site Config';
- state.processStatus.currentStep = '';
- }
- }
- } else if (progress.step === 'extract' && progress.status === 'start') {
- state.processStatus.currentStep = 'Configuring the Library...';
- } else if (progress.step === 'generate' && progress.status === 'start') {
- state.processStatus.blockDocs.status = 'processing';
- state.processStatus.currentStep = 'Configuring the Library...';
- } else if (progress.step === 'upload') {
- if (progress.status === 'start') {
- state.processStatus.blockDocs.status = 'processing';
- state.processStatus.currentStep = 'Configuring the Library...';
- } else if (progress.current && progress.total) {
- state.processStatus.blockDocs.created = progress.current;
- state.processStatus.blockDocs.status = 'processing';
- } else if (progress.status === 'complete') {
- const uploadSuccessCount = progress.uploadResults.filter((r) => r.success).length;
- const uploadErrorCount = progress.uploadResults.length - uploadSuccessCount;
-
- state.processResults = progress.uploadResults;
- state.processStatus.blockDocs.created = uploadSuccessCount;
- state.processStatus.blockDocs.status = uploadErrorCount > 0 ? 'warning' : 'complete';
- state.processStatus.currentStep = '';
-
- if (uploadErrorCount > 0) {
- state.processStatus.errors.count += uploadErrorCount;
- progress.uploadResults
- .filter((r) => !r.success)
- .forEach((r) => state.processStatus.errors.messages.push({
- type: 'upload',
- block: r.name,
- message: r.error,
- }));
- }
- }
- } else if (progress.step === 'blocks-json') {
- if (state.processStatus.blocksJson) {
- if (progress.status === 'start') {
- state.processStatus.blocksJson.status = 'processing';
- state.processStatus.blocksJson.message = state.libraryExists
- ? 'Updating...'
- : 'Creating...';
- state.processStatus.currentStep = 'Configuring the Library...';
- } else if (progress.status === 'complete') {
- state.processStatus.blocksJson.status = 'complete';
- state.processStatus.blocksJson.message = state.libraryExists
- ? 'Updated'
- : 'Created';
- state.processStatus.currentStep = '';
- }
- }
- }
-
- this.render();
- },
-
- toggleAllBlocks() {
- if (state.selectedBlocks.size === state.blocks.length) {
- state.selectedBlocks.clear();
- } else {
- state.blocks.forEach((block) => state.selectedBlocks.add(block.name));
- }
- this.render();
- },
-
- selectNewBlocksOnly() {
- state.selectedBlocks.clear();
- state.blocks
- .filter((block) => block.isNew)
- .forEach((block) => state.selectedBlocks.add(block.name));
- this.render();
- },
-
- getAllSites() {
- return [state.site];
- },
-
- async validateSite(org, site, token) {
- try {
- const response = await fetch(
- `https://admin.da.live/list/${org}/${site}/`,
- {
- method: 'GET',
- headers: { Authorization: `Bearer ${token}` },
- },
- );
- return response.ok;
- } catch (error) {
- return false;
- }
- },
-
- async openPagePicker(site) {
- if (!state.daToken) {
- state.errors.pages = 'DA.live authentication required. This tool must be run from within DA.live.';
- this.render();
- return;
- }
-
- const siteValid = await this.validateSite(state.org, site, state.daToken);
- if (!siteValid) {
- state.errors.pages = `Site "${state.org}/${site}" not found in DA.live. Please verify the site name.`;
- this.render();
- return;
- }
-
- state.currentSite = site;
- state.loadingPages = true;
- state.showPagePicker = true;
- this.renderPagePickerModal();
-
- try {
- const pages = await pagesOps.fetchSitePages(state.org, site);
- state.allPages = pages;
- state.loadingPages = false;
- this.renderPagePickerModal();
- } catch (error) {
- const errorMsg = error.message.includes('401')
- ? 'Authentication failed. Please ensure you are logged in to DA.live.'
- : `Failed to load pages: ${error.message}`;
-
- state.errors.pages = errorMsg;
- state.loadingPages = false;
- state.showPagePicker = false;
- this.render();
- }
- },
-
- renderPagePickerModal() {
- const modal = templates.pagePickerModalTemplate({
- site: state.currentSite,
- items: state.allPages, // Pass items directly
- selectedPages: state.pageSelections[state.currentSite] || new Set(),
- loading: state.loadingPages,
- });
-
- let modalContainer = document.getElementById('page-picker-modal');
- if (!modalContainer) {
- modalContainer = document.createElement('div');
- modalContainer.id = 'page-picker-modal';
- document.body.appendChild(modalContainer);
- }
-
- modalContainer.innerHTML = modal;
- this.attachPagePickerListeners();
- },
-
- attachPagePickerListeners() {
- const overlay = document.querySelector('.modal-overlay');
- if (overlay) {
- overlay.addEventListener('click', (e) => {
- if (e.target === overlay) {
- this.closePagePicker();
- }
- });
- }
-
- const cancelBtn = document.querySelector('.modal-cancel');
- if (cancelBtn) {
- cancelBtn.addEventListener('click', () => this.closePagePicker());
- }
-
- const confirmBtn = document.querySelector('.modal-confirm');
- if (confirmBtn) {
- confirmBtn.addEventListener('click', () => this.confirmPageSelection());
- }
-
- const searchInput = document.getElementById('page-search');
- if (searchInput) {
- searchInput.addEventListener('input', (e) => {
- state.pageSearchQuery = e.target.value;
- this.renderPagePickerModal();
- });
- }
-
- document.querySelectorAll('.folder-toggle').forEach((folderBtn) => {
- folderBtn.addEventListener('click', async (e) => {
- const button = e.currentTarget;
- const folderItem = button.closest('.folder-item');
- const contents = folderItem.querySelector('.folder-contents');
- const arrow = button.querySelector('.toggle-arrow');
- const icon = button.querySelector('.folder-icon');
- const { folderPath } = button.dataset;
- const isLoaded = contents.dataset.loaded === 'true';
-
- if (contents.classList.contains('hidden')) {
- contents.classList.remove('hidden');
- arrow.textContent = '▼';
- icon.textContent = '📂';
-
- if (!isLoaded) {
- try {
- const items = await pagesOps.loadFolderContents(
- state.org,
- state.currentSite,
- folderPath,
- );
-
- const childHTML = items
- .sort((a, b) => {
- if (!a.ext && b.ext) return -1;
- if (a.ext && !b.ext) return 1;
- return a.name.localeCompare(b.name);
- })
- .map((item) => {
- if (item.ext === 'html') {
- const siteSelections = state.pageSelections[state.currentSite] || new Set();
- const isSelected = siteSelections.has(item.path);
- const displayName = item.name.replace('.html', '');
- return `
-
-
-
- 📄
- ${displayName}
-
-
- `;
- }
- return `
-
-
- 📁
- ${item.name}
- ▶
-
-
-
- `;
- })
- .join('');
-
- contents.innerHTML = childHTML || 'Empty folder
';
- contents.dataset.loaded = 'true';
-
- this.attachPagePickerListeners();
- } catch (error) {
- contents.innerHTML = 'Failed to load
';
- }
- }
- } else {
- contents.classList.add('hidden');
- arrow.textContent = '▶';
- icon.textContent = '📁';
- }
- });
- });
-
- document.querySelectorAll('.page-checkbox input[type="checkbox"]').forEach((checkbox) => {
- checkbox.addEventListener('change', (e) => {
- const { path } = e.target.dataset;
- if (!state.pageSelections[state.currentSite]) {
- state.pageSelections[state.currentSite] = new Set();
- }
-
- if (e.target.checked) {
- state.pageSelections[state.currentSite].add(path);
- } else {
- state.pageSelections[state.currentSite].delete(path);
- }
-
- const confirmButton = document.querySelector('.modal-confirm');
- if (confirmButton) {
- const count = state.pageSelections[state.currentSite].size;
- confirmButton.textContent = `Confirm (${count} selected)`;
- }
- });
- });
- },
-
- closePagePicker() {
- const modalContainer = document.getElementById('page-picker-modal');
- if (modalContainer) {
- modalContainer.remove();
- }
- state.showPagePicker = false;
- state.pageSearchQuery = '';
- },
-
- confirmPageSelection() {
- this.closePagePicker();
- this.render();
- },
-
- removePage(site, path) {
- if (state.pageSelections[site]) {
- state.pageSelections[site].delete(path);
- }
- this.render();
- },
-
- showErrorModal() {
- const modal = document.getElementById('error-modal');
- if (modal) {
- modal.style.display = 'flex';
- }
- },
-
- hideErrorModal() {
- const modal = document.getElementById('error-modal');
- if (modal) {
- modal.style.display = 'none';
- }
- },
-};
-
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => app.init());
-} else {
- app.init();
-}
-
-export default app;
diff --git a/tools/library-setup/app/state.js b/tools/library-setup/app/state.js
deleted file mode 100644
index 547f387..0000000
--- a/tools/library-setup/app/state.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const state = {
- mode: 'setup',
- daToken: null,
- githubUrl: '',
- githubToken: null,
- needsToken: false,
- org: '',
- repo: '',
- site: '',
- validating: false,
- repositoryValidated: false,
- blocks: [],
- selectedBlocks: new Set(),
- discovering: false,
- blocksDiscovered: false,
- libraryExists: false,
- showPagePicker: false,
- currentSite: '',
- pageSearchQuery: '',
- allPages: [],
- loadingPages: false,
- loadedFolders: {},
- pageSelections: {},
- processing: false,
- processStatus: {
- github: { org: '', repo: '', status: 'pending' },
- blocks: { total: 0, status: 'pending' },
- siteConfig: { status: 'pending', message: '' },
- blockDocs: { created: 0, total: 0, status: 'pending' },
- blocksJson: { status: 'pending', message: '' },
- errors: { count: 0, messages: [] },
- currentStep: '',
- },
- processResults: [],
- message: '',
- messageType: 'info',
- errors: {
- github: '',
- site: '',
- blocks: '',
- pages: '',
- },
-};
-
-export default state;
diff --git a/tools/library-setup/app/templates.js b/tools/library-setup/app/templates.js
deleted file mode 100644
index 569c198..0000000
--- a/tools/library-setup/app/templates.js
+++ /dev/null
@@ -1,728 +0,0 @@
-import {
- getLibraryBlocksURL,
- getBlockEditURL,
-} from '../config.js';
-
-export function appTemplate(content) {
- return `
-
- `;
-}
-
-export function messageTemplate(message, type) {
- if (!message) return '';
- return `
-
- ${message}
-
- `;
-}
-
-export function modeToggleTemplate({ currentMode }) {
- return `
-
-
- Library Setup
-
-
- Refresh Documentation
-
-
- `;
-}
-
-export function githubSectionTemplate({
- isValidated,
- validating,
- githubUrl,
- message,
-}) {
- return `
-
- `;
-}
-
-export function tokenInputTemplate({ hasSavedToken }) {
- return `
-
- `;
-}
-
-export function siteSectionTemplate({
- org,
- site,
- message,
- mode = 'setup',
-}) {
- const isRefreshMode = mode === 'refresh';
-
- return `
-
- `;
-}
-
-export function blocksListTemplate({
- blocks,
- selectedBlocks,
- message,
-}) {
- const selectedCount = selectedBlocks.size;
- const totalCount = blocks.length;
- const newCount = blocks.filter((b) => b.isNew).length;
- const newCountText = newCount > 0 ? ` (${newCount} new)` : '';
-
- return `
-
- `;
-}
-
-export function pagesSelectionTemplate({
- allSites,
- pageSelections,
- message,
- daToken,
- org,
- mode = 'setup',
-}) {
- const totalSelectedPages = Object.values(pageSelections).reduce((sum, set) => sum + set.size, 0);
- const isRefreshMode = mode === 'refresh';
- const buttonDisabled = isRefreshMode && totalSelectedPages === 0;
-
- return `
-
- `;
-}
-
-export function initialStatusTemplate({
- org,
- repo,
- blocksCount,
- mode = 'setup',
- libraryExists = false,
-}) {
- return `
-
- `;
-}
-
-export function processingTemplate({
- processStatus,
-}) {
- return `
-
- `;
-}
-
-export function pagePickerModalTemplate({
- site,
- items,
- selectedPages,
- loading,
-}) {
- const renderItem = (item) => {
- if (item.ext === 'html') {
- const isSelected = selectedPages.has(item.path);
- const displayName = item.name.replace('.html', '');
- return `
-
-
-
- 📄
- ${displayName}
-
-
- `;
- }
- return `
-
-
- 📁
- ${item.name}
- ▶
-
-
-
- `;
- };
-
- return `
-
-
-
Select Pages from ${site}
-
- ${loading ? `
-
- ` : `
-
- ${!items || items.length === 0 ? `
-
No pages found
- ` : items
- .sort((a, b) => {
- if (!a.ext && b.ext) return -1;
- if (a.ext && !b.ext) return 1;
- return a.name.localeCompare(b.name);
- })
- .map(renderItem)
- .join('')}
-
-
-
- Cancel
-
- Confirm (${selectedPages.size} selected)
-
-
- `}
-
-
- `;
-}
-
-export function finalStatusTemplate({ processStatus, org, repo }) {
- return `
-
- `;
-}
-
-export function resultsTemplate({
- processStatus,
- processResults,
-}) {
- const successResults = processResults.filter((r) => r.success);
- const errorResults = processResults.filter((r) => !r.success);
-
- return `
-
- `;
-}
-
-export function errorModalTemplate(errors) {
- if (!errors || errors.length === 0) return '';
-
- const uploadErrors = errors.filter((e) => e.type === 'upload');
- const generalErrors = errors.filter((e) => e.type === 'general');
-
- const formatErrorStatus = (message) => {
- const match = message.match(/(\d{3})/);
- return match ? match[1] : 'Error';
- };
-
- const formatErrorType = (message) => {
- if (message.includes('Upload failed')) return 'Upload Failed';
- if (message.includes('403')) return 'Permission Denied';
- if (message.includes('404')) return 'Not Found';
- return 'Error';
- };
-
- return `
-
-
-
-
- ${generalErrors.length > 0 ? `
-
-
General Errors
-
-
-
- Error
-
-
-
- ${generalErrors.map((e) => `
-
- ${e.message}
-
- `).join('')}
-
-
-
- ` : ''}
-
- ${uploadErrors.length > 0 ? `
-
-
Upload Errors
-
-
-
- Block
- Status
- Error
-
-
-
- ${uploadErrors.map((e) => `
-
- ${e.block}
- ${formatErrorStatus(e.message)}
- ${formatErrorType(e.message)}
-
- `).join('')}
-
-
-
- ` : ''}
-
-
-
- `;
-}
diff --git a/tools/library-setup/library-setup.css b/tools/library-setup/library-setup.css
deleted file mode 100644
index 6a7c393..0000000
--- a/tools/library-setup/library-setup.css
+++ /dev/null
@@ -1,1176 +0,0 @@
-/* Library Setup - Clean styles inspired by DA.live Importer */
-
-:root {
- /* Colors - matching DA.live */
- --s2-gray-50: #f9fafb;
- --s2-gray-200: #e5e7eb;
- --s2-gray-600: #4b5563;
- --s2-gray-900: #111827;
- --s2-blue-200: #bfdbfe;
- --s2-blue-900: #1e3a8a;
- --s2-green-100: rgb(215 247 225);
- --s2-green-200: rgb(202 255 164);
- --s2-cyan-100: rgb(202 248 250);
- --s2-blue-100: rgb(181 230 252);
- --s2-red-100: rgb(255 214 209);
-
- /* Spacing */
- --spacing-100: 4px;
- --spacing-200: 8px;
- --spacing-300: 12px;
- --spacing-400: 16px;
- --spacing-500: 24px;
- --spacing-600: 32px;
- --spacing-800: 48px;
-
- /* Border radius */
- --s2-radius-100: 4px;
- --s2-radius-200: 8px;
- --s2-radius-300: 18px;
-
- /* Typography */
- --body-font-family: 'Adobe Clean', adobe-clean, 'Trebuchet MS', sans-serif;
- --mono-font-family: 'Roboto Mono', menlo, consolas, 'Liberation Mono', monospace;
- --s2-font-size-200: 14px;
- --s2-font-size-400: 16px;
- --s2-font-size-600: 24px;
-}
-
-* {
- box-sizing: border-box;
-}
-
-body {
- font-family: var(--body-font-family);
- color: var(--s2-gray-900);
- line-height: 1.6;
- margin: 0;
- padding: 0;
-}
-
-.library-setup-container {
- max-width: 1200px;
- margin: var(--spacing-800) auto;
- padding: 0 var(--spacing-400);
-}
-
-.library-setup-header {
- margin-bottom: var(--spacing-600);
-}
-
-.library-setup-header h1 {
- font-size: 32px;
- line-height: 1.2;
- margin: 0 0 var(--spacing-200);
-}
-
-.library-setup-header p {
- font-size: var(--s2-font-size-400);
- color: var(--s2-gray-600);
- margin: 0;
-}
-
-/* Mode Toggle */
-.mode-toggle {
- display: flex;
- gap: var(--spacing-200);
- margin-bottom: var(--spacing-600);
- padding: var(--spacing-200);
- background: var(--s2-gray-200);
- border-radius: var(--s2-radius-200);
- width: fit-content;
- border: 2px solid var(--s2-gray-200);
-}
-
-.mode-btn {
- padding: var(--spacing-200) var(--spacing-500);
- background: transparent;
- border: 2px solid transparent;
- border-radius: var(--s2-radius-100);
- font-size: var(--s2-font-size-200);
- font-weight: 600;
- color: var(--s2-gray-600);
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.mode-btn:hover {
- background: white;
- color: var(--s2-gray-900);
-}
-
-.mode-btn.active {
- background: white;
- color: #000;
- font-weight: 700;
- border: 2px solid #000;
- box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
-}
-
-/* Form Section */
-.form-section {
- background: white;
- padding: var(--spacing-300) 0;
- margin-bottom: var(--spacing-300);
-}
-
-.form-section h2 {
- font-size: 20px;
- margin: 0 0 var(--spacing-200);
-}
-
-.form-section-subtitle {
- font-size: var(--s2-font-size-200);
- color: var(--s2-gray-600);
- margin: 0 0 var(--spacing-500);
-}
-
-/* Info Banner */
-.info-banner {
- background: var(--s2-blue-200);
- color: var(--s2-blue-900);
- border-radius: var(--s2-radius-100);
- padding: var(--spacing-300) var(--spacing-400);
- margin-bottom: var(--spacing-400);
- font-size: var(--s2-font-size-200);
-}
-
-/* Preview Notice */
-.preview-notice {
- background: var(--s2-blue-100);
- border-left: 4px solid var(--s2-blue-500);
- border-radius: var(--s2-radius-100);
- padding: var(--spacing-400);
- margin: var(--spacing-500) 0;
-}
-
-.preview-notice-content {
- font-size: var(--s2-font-size-200);
- color: var(--s2-gray-900);
-}
-
-.preview-notice-content h3 {
- margin: 0 0 var(--spacing-200) 0;
- color: var(--s2-blue-900);
- font-size: 18px;
-}
-
-.preview-notice-content p {
- margin: 0 0 var(--spacing-200) 0;
- line-height: 1.5;
-}
-
-.preview-notice-content strong {
- color: var(--s2-blue-900);
-}
-
-.preview-notice-link {
- display: inline-block;
- margin-top: var(--spacing-100);
- color: var(--s2-blue-700);
- font-weight: 600;
- text-decoration: none;
-}
-
-.preview-notice-link:hover {
- text-decoration: underline;
-}
-
-/* Form Elements */
-.form-row {
- margin-bottom: var(--spacing-500);
-}
-
-.repo-input-row {
- position: relative;
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
-}
-
-.org-site-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: var(--spacing-400);
- margin-bottom: var(--spacing-400);
-}
-
-.input-group {
- display: flex;
- flex-direction: column;
-}
-
-.readonly-input {
- background: var(--s2-gray-100);
- color: var(--s2-gray-600);
- cursor: not-allowed;
-}
-
-label {
- display: block;
- font-weight: 700;
- font-size: var(--s2-font-size-200);
- margin-bottom: var(--spacing-200);
-}
-
-.checkbox-label {
- display: flex;
- align-items: flex-start;
- gap: var(--spacing-200);
- font-weight: 400;
- cursor: pointer;
- margin-bottom: 0;
-}
-
-.checkbox-label input[type="checkbox"] {
- margin-top: 2px;
- cursor: pointer;
-}
-
-.checkbox-label span {
- flex: 1;
- font-size: var(--s2-font-size-100);
- color: var(--s2-gray-700);
-}
-
-.label-hint {
- font-weight: 400;
- color: var(--s2-gray-600);
- font-size: 13px;
-}
-
-.token-input-section {
- background: var(--s2-gray-50);
- border: 1px solid var(--s2-gray-200);
- border-radius: var(--s2-border-radius-200);
- padding: var(--spacing-400);
- margin-top: var(--spacing-300);
-}
-
-.token-content {
- display: grid;
- grid-template-columns: 60% 40%;
- gap: var(--spacing-500);
- align-items: start;
-}
-
-@media (width <= 900px) {
- .token-content {
- grid-template-columns: 1fr;
- }
-
- .token-instructions {
- border-left: none !important;
- border-top: 1px solid var(--s2-gray-300);
- padding-left: 0 !important;
- padding-top: var(--spacing-400) !important;
- }
-}
-
-.token-form {
- flex: 1;
-}
-
-.token-form .form-row {
- margin-bottom: var(--spacing-300);
-}
-
-.token-form .form-row:last-child {
- margin-bottom: 0;
-}
-
-.token-form .button-row {
- display: flex;
- gap: var(--spacing-300);
- align-items: center;
- flex-wrap: wrap;
-}
-
-.token-instructions {
- padding-left: var(--spacing-500);
- border-left: 1px solid var(--s2-gray-300);
-}
-
-.token-instructions h4 {
- margin: 0 0 var(--spacing-200) 0;
- font-size: 14px;
- font-weight: 600;
- color: var(--s2-gray-900);
-}
-
-.token-instructions p {
- margin: 0 0 var(--spacing-200) 0;
- font-size: 13px;
- color: var(--s2-gray-700);
-}
-
-.token-instructions ul {
- margin: 0 0 var(--spacing-400) 0;
- padding-left: var(--spacing-400);
- list-style: none;
-}
-
-.token-instructions li {
- font-size: 13px;
- color: var(--s2-gray-700);
- margin-bottom: var(--spacing-200);
- padding-left: var(--spacing-300);
- position: relative;
-}
-
-.token-instructions li::before {
- content: "•";
- position: absolute;
- left: 0;
- color: var(--s2-gray-500);
-}
-
-.site-label strong {
- color: #000;
-}
-
-.token-instructions li strong {
- color: var(--s2-gray-900);
- font-weight: 600;
- font-family: var(--mono-font-family);
- font-size: 12px;
-}
-
-.token-instructions .permission-note {
- font-size: 12px;
- color: var(--s2-gray-600);
- font-style: italic;
- margin-top: var(--spacing-300);
-}
-
-.create-token-btn {
- display: inline-block;
- font-family: var(--body-font-family);
- font-size: 15px;
- font-weight: 700;
- padding: 8px 24px;
- line-height: 18px;
- border: 2px solid #000;
- color: #000;
- background: white;
- border-radius: var(--s2-radius-300);
- text-decoration: none;
- cursor: pointer;
- transition: all 0.2s;
- text-align: center;
-}
-
-.create-token-btn:hover {
- background: rgb(245 245 245);
-}
-
-.token-input {
- font-family: var(--mono-font-family);
- letter-spacing: 0.5px;
- font-size: 14px;
- padding: 10px 12px;
- background: white;
- border: 1px solid var(--s2-gray-300);
- border-radius: var(--s2-border-radius-200);
- width: 100%;
- transition: border-color 0.15s ease;
-}
-
-.token-input:focus {
- outline: none;
- border-color: var(--s2-blue-500);
- box-shadow: 0 0 0 3px rgb(59 99 251 / 10%);
-}
-
-.token-input::placeholder {
- color: var(--s2-gray-400);
- letter-spacing: 1px;
-}
-
-input[type="text"],
-input[type="url"] {
- font-family: var(--body-font-family);
- display: block;
- background: var(--s2-gray-50);
- border: 2px solid var(--s2-gray-200);
- border-radius: var(--s2-radius-100);
- line-height: 32px;
- padding: 0 var(--spacing-300);
- width: 100%;
- font-size: var(--s2-font-size-200);
- transition: border-color 0.2s;
-}
-
-input[type="text"]:focus,
-input[type="url"]:focus {
- outline: none;
- border-color: var(--s2-blue-900);
-}
-
-input[readonly] {
- background: var(--s2-gray-200);
- cursor: not-allowed;
-}
-
-/* Validation Success */
-
-/* Blocks List */
-.blocks-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--spacing-400);
-}
-
-.blocks-header h2 {
- margin: 0;
- display: flex;
- align-items: baseline;
- gap: var(--spacing-200);
-}
-
-.blocks-actions {
- display: flex;
- gap: var(--spacing-200);
-}
-
-.heading-annotation {
- font-size: 16px;
- font-weight: 400;
- color: #676767;
-}
-
-.blocks-list {
- list-style: none;
- padding: 0;
- margin: 0;
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: var(--spacing-300);
-}
-
-.block-item {
- background: var(--s2-gray-50);
- border: 2px solid var(--s2-gray-200);
- border-radius: var(--s2-radius-100);
- padding: var(--spacing-300);
- transition: all 0.2s;
-}
-
-.block-item:hover {
- border-color: var(--s2-blue-900);
-}
-
-.block-item.selected {
- background: var(--s2-blue-200);
- border-color: var(--s2-blue-900);
-}
-
-.block-item label {
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
- cursor: pointer;
- margin: 0;
- font-weight: 400;
-}
-
-.block-item input[type="checkbox"] {
- cursor: pointer;
-}
-
-.block-name {
- font-family: var(--mono-font-family);
- font-size: 13px;
- flex: 1;
-}
-
-.block-badge {
- display: inline-block;
- padding: 2px 8px;
- font-size: 11px;
- font-weight: 600;
- border-radius: var(--s2-radius-100);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.block-badge.new {
- background: white;
- color: var(--s2-gray-900);
- border: 1px solid var(--s2-gray-200);
-}
-
-/* Buttons (Import tool style) */
-button,
-.button {
- font-family: var(--body-font-family);
- font-size: 15px;
- font-weight: 700;
- padding: 8px 24px;
- line-height: 18px;
- border: 2px solid #000;
- color: #000;
- border-radius: var(--s2-radius-300);
- background: none;
- cursor: pointer;
- transition: all 0.2s;
- text-align: center;
-}
-
-.action,
-button.action {
- line-height: 1;
- padding: 4px 8px;
- font-size: 14px;
- font-weight: 400;
- border-radius: var(--s2-radius-100);
- background: rgb(225 225 225);
- border: 2px solid rgb(225 225 225);
- color: #000;
-}
-
-.action:hover,
-button.action:hover {
- background: rgb(200 200 200);
- border: 2px solid rgb(200 200 200);
-}
-
-button.accent,
-.button.accent {
- background: #3b63fb;
- border: 2px solid #3b63fb;
- color: #fff;
-}
-
-button:disabled {
- background-color: #efefef;
- border: 2px solid #efefef;
- color: var(--s2-gray-700);
- cursor: not-allowed;
-}
-
-.button-group {
- display: flex;
- gap: var(--spacing-300);
- margin-top: var(--spacing-500);
-}
-
-/* Import Tool Style Cards */
-.import-card {
- border-radius: 18px;
- padding: var(--spacing-400);
- border: 2px dashed rgb(0 0 0 / 20%);
- transition: all 0.2s;
-}
-
-/* Fixed color scheme - each card has its own color */
-.import-card-blue {
- background: rgb(181 230 252);
-}
-
-.import-card-lime {
- background: rgb(202 255 164);
-}
-
-.import-card-green {
- background: rgb(215 247 225);
-}
-
-.import-card-cyan {
- background: rgb(202 248 250);
-}
-
-.import-card-red {
- background: rgb(255 214 209);
-}
-
-.import-card-header {
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
- margin-bottom: var(--spacing-300);
-}
-
-.import-card h3 {
- margin: 0;
- font-size: var(--s2-font-size-300);
- font-weight: 600;
-}
-
-.import-card-body p {
- margin: 0 0 var(--spacing-200);
- font-size: var(--s2-font-size-200);
-}
-
-.import-card-value {
- font-size: var(--s2-font-size-700);
- font-weight: 700;
- line-height: 1;
- margin: 0;
-}
-
-.repo-path {
- overflow-wrap: break-word;
- word-break: break-all;
- font-size: var(--s2-font-size-200);
-}
-
-.import-card-link {
- display: inline-block;
- color: #1e40af;
- text-decoration: none;
- font-size: 14px;
- font-weight: 500;
- margin-top: var(--spacing-200);
-}
-
-.import-card-link:hover {
- text-decoration: underline;
-}
-
-/* Status Cards Grid */
-.status-cards-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: var(--spacing-400);
- margin: var(--spacing-500) 0;
-}
-
-.view-errors-link {
- color: #991b1b !important;
- margin-top: var(--spacing-300);
-}
-
-.error-details {
- margin-top: var(--spacing-300);
- font-size: 13px;
-}
-
-.error-details summary {
- cursor: pointer;
- color: #991b1b;
- font-weight: 600;
-}
-
-.error-list {
- margin: var(--spacing-200) 0 0;
- padding-left: var(--spacing-400);
- list-style: disc;
-}
-
-.error-list li {
- margin-bottom: var(--spacing-100);
- color: #991b1b;
-}
-
-/* Status Banners (Different from status-message in cards) */
-.status-banner {
- padding: var(--spacing-300) var(--spacing-400);
- border-radius: var(--s2-radius-100);
- margin: var(--spacing-400) 0;
- font-size: var(--s2-font-size-200);
-}
-
-.status-banner.info {
- background: var(--s2-blue-200);
- color: var(--s2-blue-900);
-}
-
-.status-banner.error {
- background: var(--s2-red-100);
- color: #991b1b;
-}
-
-.status-banner.success {
- background: var(--s2-green-100);
- color: #065f46;
-}
-
-/* Loading Spinner */
-.loading {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: var(--spacing-600);
-}
-
-.spinner {
- width: 32px;
- height: 32px;
- border: 3px solid var(--s2-gray-200);
- border-top-color: var(--s2-blue-900);
- border-radius: 50%;
- animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-.loading p {
- margin-top: var(--spacing-300);
- color: var(--s2-gray-600);
-}
-
-/* Current Step Display */
-.current-step {
- font-size: var(--s2-font-size-400);
- font-weight: 500;
- margin: var(--spacing-400) 0 0;
- line-height: 1.6;
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
-}
-
-.current-step.processing::before {
- content: '';
- display: inline-block;
- width: 16px;
- height: 16px;
- border: 2px solid var(--s2-gray-300);
- border-top-color: var(--s2-gray-600);
- border-radius: 50%;
- animation: spinner 0.8s linear infinite;
-}
-
-@keyframes spinner {
- to { transform: rotate(360deg); }
-}
-
-/* Results - Block Cards */
-.results-section {
- margin: var(--spacing-600) 0;
-}
-
-.results-section h3 {
- margin: 0 0 var(--spacing-400);
- font-size: 18px;
-}
-
-.block-cards {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: var(--spacing-400);
-}
-
-.block-card {
- background: white;
- border: 2px solid var(--s2-gray-200);
- border-radius: var(--s2-radius-200);
- padding: var(--spacing-400);
- display: flex;
- gap: var(--spacing-300);
-}
-
-.block-card.success {
- border-color: #10b981;
- background: var(--s2-green-100);
-}
-
-.block-card.error {
- border-color: #ef4444;
- background: var(--s2-red-100);
-}
-
-.block-card-icon {
- font-size: 24px;
- font-weight: 700;
- flex-shrink: 0;
-}
-
-.block-card.success .block-card-icon {
- color: #10b981;
-}
-
-.block-card.error .block-card-icon {
- color: #ef4444;
-}
-
-.block-card-content {
- flex: 1;
-}
-
-.block-card-name {
- font-family: var(--mono-font-family);
- font-size: var(--s2-font-size-400);
- margin: 0 0 var(--spacing-200);
-}
-
-.block-card-link {
- color: var(--s2-blue-900);
- text-decoration: none;
- font-size: 13px;
-}
-
-.block-card-link:hover {
- text-decoration: underline;
-}
-
-.block-card-error {
- font-size: 13px;
- color: #991b1b;
- margin: 0;
-}
-
-/* Site Section */
-.site-section {
- margin-bottom: var(--spacing-500);
-}
-
-.site-selection {
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
-}
-
-.site-label {
- font-size: var(--s2-font-size-300);
- color: #676767;
-}
-
-.selected-pages-list {
- margin-top: var(--spacing-300);
-}
-
-.selected-page-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: var(--s2-gray-50);
- padding: var(--spacing-200) var(--spacing-300);
- border-radius: var(--s2-radius-100);
- margin-bottom: var(--spacing-200);
-}
-
-.page-path {
- font-family: var(--mono-font-family);
- font-size: 13px;
-}
-
-.remove-page-btn {
- background: transparent;
- border: none;
- color: #ef4444;
- font-size: 20px;
- cursor: pointer;
- padding: 0;
- line-height: 1;
- width: 24px;
- height: 24px;
-}
-
-/* Modal */
-.modal-overlay {
- position: fixed;
- inset: 0;
- background: rgb(0 0 0 / 50%);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
-}
-
-.modal {
- background: white;
- border-radius: var(--s2-radius-200);
- padding: var(--spacing-600);
- max-width: 600px;
- width: 90%;
- max-height: 80vh;
- overflow-y: auto;
- box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
-}
-
-.modal h2 {
- margin-top: 0;
-}
-
-.page-list {
- max-height: 400px;
- overflow-y: auto;
- border: 2px solid var(--s2-gray-200);
- border-radius: var(--s2-radius-100);
- padding: var(--spacing-400);
- margin: var(--spacing-400) 0;
-}
-
-/* Tree View Styles */
-.tree-view {
- padding: 0;
-}
-
-.tree-item {
- margin: 0;
-}
-
-.folder-item {
- margin-bottom: var(--spacing-200);
-}
-
-.folder-toggle {
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
- width: 100%;
- padding: var(--spacing-200);
- border: none;
- background: transparent;
- cursor: pointer;
- font-family: var(--body-font-family);
- font-size: var(--s2-font-size-200);
- text-align: left;
- border-radius: var(--s2-radius-100);
- transition: background 0.2s;
-}
-
-.folder-toggle:hover {
- background: var(--s2-gray-50);
-}
-
-.folder-icon {
- flex-shrink: 0;
- font-size: 16px;
-}
-
-.folder-name {
- flex: 1;
- font-weight: 500;
-}
-
-.toggle-arrow {
- flex-shrink: 0;
- font-size: 12px;
- transition: transform 0.2s;
-}
-
-.folder-contents {
- margin-left: var(--spacing-300);
-}
-
-.folder-contents.hidden {
- display: none;
-}
-
-.folder-loading {
- padding: var(--spacing-200) var(--spacing-300);
- color: var(--s2-gray-600);
- font-size: var(--s2-font-size-100);
- font-style: italic;
-}
-
-.file-item {
- margin-bottom: 2px;
-}
-
-.page-checkbox {
- display: flex;
- align-items: center;
- gap: var(--spacing-200);
- padding: var(--spacing-200);
- cursor: pointer;
- font-size: var(--s2-font-size-200);
- transition: background 0.2s;
- border-radius: var(--s2-radius-100);
- width: 100%;
-}
-
-.page-checkbox:hover {
- background: var(--s2-gray-50);
-}
-
-.page-checkbox.selected {
- background: var(--s2-blue-200);
-}
-
-.page-checkbox input[type="checkbox"] {
- cursor: pointer;
- flex-shrink: 0;
-}
-
-.page-icon {
- flex-shrink: 0;
- font-size: 14px;
-}
-
-.page-name {
- flex: 1;
- font-family: var(--mono-font-family);
- font-size: 13px;
-}
-
-.error-modal-overlay {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgb(0 0 0 / 50%);
- z-index: 1000;
- align-items: center;
- justify-content: center;
-}
-
-.error-modal {
- background: white;
- border-radius: 8px;
- max-width: 900px;
- width: 90%;
- max-height: 80vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
-}
-
-.error-modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--spacing-400);
- border-bottom: 1px solid var(--s2-gray-200);
-}
-
-.error-modal-header h2 {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
-}
-
-.error-modal-close {
- background: none;
- border: none;
- font-size: 28px;
- line-height: 1;
- cursor: pointer;
- color: var(--s2-gray-600);
- padding: 0;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.error-modal-close:hover {
- color: var(--s2-gray-900);
-}
-
-.error-modal-content {
- flex: 1;
- overflow-y: auto;
- padding: var(--spacing-400);
-}
-
-.error-section {
- margin-bottom: var(--spacing-400);
-}
-
-.error-section:last-child {
- margin-bottom: 0;
-}
-
-.error-section h3 {
- margin: 0 0 var(--spacing-200) 0;
- font-size: 16px;
- font-weight: 600;
- color: var(--s2-gray-900);
-}
-
-.error-note {
- margin: 0 0 var(--spacing-200) 0;
- padding: var(--spacing-200);
- background: var(--s2-blue-50);
- border-left: 3px solid var(--s2-blue-900);
- font-size: 14px;
- color: var(--s2-gray-700);
-}
-
-.error-modal-list {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-
-.error-modal-list li {
- padding: var(--spacing-300);
- margin-bottom: var(--spacing-200);
- background: var(--s2-gray-50);
- border-radius: 4px;
- font-size: 14px;
- line-height: 1.5;
-}
-
-.error-list li:last-child {
- margin-bottom: 0;
-}
-
-.error-list li strong {
- color: var(--s2-gray-900);
- font-weight: 600;
-}
-
-.error-modal-footer {
- padding: var(--spacing-400);
- border-top: 1px solid var(--s2-gray-200);
- display: flex;
- justify-content: flex-end;
-}
-
-.import-card.errors {
- transition: transform 0.2s ease;
-}
-
-.import-card.errors:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgb(0 0 0 / 10%);
-}
-
-/* Error Table Styles */
-.error-table {
- width: 100%;
- border-collapse: collapse;
- margin-top: var(--spacing-300);
- font-size: 14px;
-}
-
-.error-table thead {
- background: var(--s2-gray-50);
-}
-
-.error-table th {
- text-align: left;
- padding: var(--spacing-300);
- font-weight: 600;
- color: var(--s2-gray-900);
- border-bottom: 2px solid var(--s2-gray-200);
-}
-
-.error-table td {
- padding: var(--spacing-300);
- border-bottom: 1px solid var(--s2-gray-200);
- color: var(--s2-gray-700);
-}
-
-.error-table tbody tr:last-child td {
- border-bottom: none;
-}
-
-.error-table tbody tr:hover {
- background: var(--s2-gray-50);
-}
-
-.error-table .block-name {
- font-family: var(--mono-font-family);
- color: var(--s2-gray-900);
- font-weight: 500;
-}
-
-.error-table .status-code {
- font-family: var(--mono-font-family);
- font-weight: 600;
- color: #991b1b;
- text-align: left;
- width: 80px;
-}
diff --git a/tools/library-setup/utils/da-api.js b/tools/library-setup/utils/da-api.js
deleted file mode 100644
index e6c6d87..0000000
--- a/tools/library-setup/utils/da-api.js
+++ /dev/null
@@ -1,418 +0,0 @@
-import state from '../app/state.js';
-import { LIBRARY_BLOCKS_PATH, CONTENT_DA_LIVE_BASE } from '../config.js';
-
-const DA_ADMIN = 'https://admin.da.live';
-
-async function daFetch(url, options = {}) {
- const token = state.daToken;
-
- const headers = {
- ...options.headers,
- };
-
- if (token) {
- headers.Authorization = `Bearer ${token}`;
- }
-
- const response = await fetch(url, { ...options, headers });
-
- return response;
-}
-
-export async function fetchSiteConfig(org, site) {
- const url = `${DA_ADMIN}/config/${org}/${site}`;
-
- try {
- const response = await daFetch(url);
-
- if (response.status === 404) {
- return null;
- }
-
- if (!response.ok) {
- throw new Error(`Failed to fetch config.json: ${response.status}`);
- }
-
- return response.json();
- } catch (error) {
- return null;
- }
-}
-
-async function checkBlockDocExists(org, site, blockName) {
- const path = `/${org}/${site}/library/blocks/${blockName}`;
- const url = `${DA_ADMIN}/source${path}.html`;
-
- try {
- const response = await daFetch(url, { method: 'HEAD' });
- return response.ok;
- } catch (error) {
- return false;
- }
-}
-
-async function createBlockDocVersion(org, site, blockName) {
- const path = `/${org}/${site}/library/blocks/${blockName}`;
- const url = `${DA_ADMIN}/versionsource${path}.html`;
-
- try {
- const response = await daFetch(url, { method: 'POST' });
- return response.ok;
- } catch (error) {
- return false;
- }
-}
-
-export async function uploadBlockDoc(org, site, blockName, htmlContent) {
- const path = `/${org}/${site}/library/blocks/${blockName}`;
- const url = `${DA_ADMIN}/source${path}.html`;
-
- try {
- const exists = await checkBlockDocExists(org, site, blockName);
- if (exists) {
- await createBlockDocVersion(org, site, blockName);
- }
-
- const formData = new FormData();
- const blob = new Blob([htmlContent], { type: 'text/html' });
- formData.set('data', blob);
-
- const response = await daFetch(url, {
- method: 'POST',
- body: formData,
- });
-
- if (!response.ok) {
- throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
- }
-
- return {
- success: true,
- path,
- error: null,
- };
- } catch (error) {
- return {
- success: false,
- path,
- error: error.message,
- };
- }
-}
-
-export async function fetchBlocksJSON(org, site) {
- let path = `${org}/${site}/library/blocks.json`;
-
- const config = await fetchSiteConfig(org, site);
- if (config?.library?.data) {
- const blocksEntry = config.library.data.find((entry) => entry.title === 'Blocks');
- if (blocksEntry?.path) {
- path = blocksEntry.path.replace('https://content.da.live/', '');
- }
- }
-
- const url = `${DA_ADMIN}/source/${path}`;
-
- try {
- const response = await daFetch(url);
-
- if (response.status === 404) {
- return null;
- }
-
- if (!response.ok) {
- throw new Error(`Failed to fetch blocks.json: ${response.status}`);
- }
-
- return response.json();
- } catch (error) {
- return null;
- }
-}
-
-export async function updateBlocksJSON(org, site, config) {
- let path = `${org}/${site}/library/blocks.json`;
-
- const siteConfig = await fetchSiteConfig(org, site);
- if (siteConfig?.library?.data) {
- const blocksEntry = siteConfig.library.data.find((entry) => entry.title === 'Blocks');
- if (blocksEntry?.path) {
- path = blocksEntry.path.replace('https://content.da.live/', '');
- }
- }
-
- const url = `${DA_ADMIN}/source/${path}`;
-
- try {
- const formData = new FormData();
- const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
- formData.set('data', blob);
-
- const response = await daFetch(url, {
- method: 'POST',
- body: formData,
- });
-
- if (!response.ok) {
- throw new Error(`Failed to update blocks.json: ${response.status}`);
- }
-
- return {
- success: true,
- error: null,
- };
- } catch (error) {
- return {
- success: false,
- error: error.message,
- };
- }
-}
-
-export async function batchUploadBlocks(org, site, blocks, onProgress, batchSize = 5) {
- const results = [];
- let processed = 0;
-
- const batches = [];
- for (let i = 0; i < blocks.length; i += batchSize) {
- batches.push({
- blocks: blocks.slice(i, i + batchSize),
- startIndex: i,
- });
- }
-
- await batches.reduce(async (previousPromise, { blocks: batch, startIndex }) => {
- await previousPromise;
-
- const processBatch = async (block, batchIndex) => {
- const currentIndex = startIndex + batchIndex;
- if (onProgress) {
- onProgress({
- current: currentIndex + 1,
- total: blocks.length,
- blockName: block.name,
- status: 'uploading',
- });
- }
-
- const result = await uploadBlockDoc(
- org,
- site,
- block.name,
- block.html,
- );
- processed += 1;
-
- if (onProgress) {
- onProgress({
- current: processed,
- total: blocks.length,
- blockName: block.name,
- status: result.success ? 'success' : 'error',
- });
- }
-
- return {
- name: block.name,
- ...result,
- };
- };
-
- const batchResults = await Promise.all(batch.map(processBatch));
- results.push(...batchResults);
- }, Promise.resolve());
-
- return results;
-}
-
-export async function validateSite(org, site) {
- const url = `https://admin.hlx.page/config/${org}/sites/${site}.json`;
-
- try {
- const response = await daFetch(url, { method: 'HEAD' });
- return response.ok;
- } catch (error) {
- return false;
- }
-}
-
-export async function updateSiteConfig(org, site, config) {
- const url = `${DA_ADMIN}/config/${org}/${site}`;
-
- try {
- const formData = new FormData();
- formData.append('config', JSON.stringify(config));
-
- const response = await daFetch(url, {
- method: 'PUT',
- body: formData,
- });
-
- if (!response.ok) {
- throw new Error(`Failed to update config.json: ${response.status}`);
- }
-
- return {
- success: true,
- error: null,
- };
- } catch (error) {
- return {
- success: false,
- error: error.message,
- };
- }
-}
-
-export async function registerLibrary(org, site) {
- try {
- let config = await fetchSiteConfig(org, site);
- const blocksPath = `${CONTENT_DA_LIVE_BASE}/${org}/${site}/${LIBRARY_BLOCKS_PATH}.json`;
- let wasCreated = false;
-
- if (!config) {
- config = {
- ':version': 3,
- ':names': ['library'],
- ':type': 'multi-sheet',
- library: {
- total: 1,
- limit: 1,
- offset: 0,
- data: [
- {
- title: 'Blocks',
- path: blocksPath,
- },
- ],
- },
- };
-
- const result = await updateSiteConfig(org, site, config);
- return {
- success: result.success,
- created: true,
- error: result.error,
- };
- }
-
- const configType = config[':type'];
-
- if (configType === 'sheet') {
- const existingData = config.data || [];
- const existingColWidths = config[':colWidths'];
-
- config = {
- ':version': 3,
- ':names': ['data', 'library'],
- ':type': 'multi-sheet',
- data: {
- total: existingData.length,
- limit: existingData.length,
- offset: 0,
- data: existingData,
- },
- library: {
- total: 1,
- limit: 1,
- offset: 0,
- data: [
- {
- title: 'Blocks',
- path: blocksPath,
- },
- ],
- },
- };
-
- if (existingColWidths) {
- config.data[':colWidths'] = existingColWidths;
- }
-
- wasCreated = true;
- } else if (configType === 'multi-sheet') {
- if (!config.library) {
- if (!config[':names'].includes('library')) {
- config[':names'].push('library');
- }
-
- config.library = {
- total: 1,
- limit: 1,
- offset: 0,
- data: [
- {
- title: 'Blocks',
- path: blocksPath,
- },
- ],
- };
-
- wasCreated = true;
- } else {
- const libraryData = config.library.data || [];
- const blocksIndex = libraryData.findIndex((entry) => entry.title === 'Blocks');
-
- if (blocksIndex === -1) {
- libraryData.push({
- title: 'Blocks',
- path: blocksPath,
- });
-
- config.library.data = libraryData;
- config.library.total = libraryData.length;
- config.library.limit = libraryData.length;
- wasCreated = true;
- } else {
- const existingPath = libraryData[blocksIndex].path;
- if (existingPath === blocksPath) {
- return {
- success: true,
- created: false,
- error: null,
- };
- }
-
- libraryData[blocksIndex].path = blocksPath;
- config.library.data = libraryData;
- }
- }
- } else {
- config = {
- ':version': 3,
- ':names': ['library'],
- ':type': 'multi-sheet',
- library: {
- total: 1,
- limit: 1,
- offset: 0,
- data: [
- {
- title: 'Blocks',
- path: blocksPath,
- },
- ],
- },
- };
-
- wasCreated = true;
- }
-
- if (!config[':version']) {
- config[':version'] = 3;
- }
-
- const result = await updateSiteConfig(org, site, config);
- return {
- success: result.success,
- created: wasCreated,
- error: result.error,
- };
- } catch (error) {
- return {
- success: false,
- created: false,
- error: error.message,
- };
- }
-}