diff --git a/.changeset/config.json b/.changeset/config.json index ae82eba8..33b0d29c 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,16 @@ "access": "restricted", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [ + "tce-counter", + "tce-counter-manifest", + "tce-counter-edit", + "tce-counter-display", + "tce-counter-server", + "tce-question", + "tce-question-manifest", + "tce-question-edit", + "tce-question-display", + "tce-question-server" + ] } diff --git a/example/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json similarity index 89% rename from example/.devcontainer/devcontainer.json rename to .devcontainer/devcontainer.json index d2c2812f..ee03343d 100644 --- a/example/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { - "name": "Tailor - Content Element", - "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + "name": "Tailor XT — Counter example", + "image": "mcr.microsoft.com/devcontainers/typescript-node:4-24-trixie", "features": { "ghcr.io/devcontainers-extra/features/pnpm:2": {}, "ghcr.io/devcontainers/features/github-cli:1": {} @@ -24,8 +24,8 @@ "onAutoForward": "silent" } }, - "postCreateCommand": "pnpm install --frozen-lockfile && pnpm build", - "postAttachCommand": "/bin/bash .devcontainer/setup.sh && pnpm dev", + "postCreateCommand": "pnpm install --frozen-lockfile && pnpm build && pnpm -C examples/counter run build", + "postAttachCommand": "/bin/bash .devcontainer/setup.sh && pnpm -C examples/counter run dev", "customizations": { "vscode": { "settings": { diff --git a/.devcontainer/question/devcontainer.json b/.devcontainer/question/devcontainer.json new file mode 100644 index 00000000..a573ff21 --- /dev/null +++ b/.devcontainer/question/devcontainer.json @@ -0,0 +1,62 @@ +{ + "name": "Tailor XT — Question example", + "image": "mcr.microsoft.com/devcontainers/typescript-node:4-24-trixie", + "features": { + "ghcr.io/devcontainers-extra/features/pnpm:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "forwardPorts": [8080, 8010, 8020, 8030], + "portsAttributes": { + "8080": { + "label": "Content Element Kit preview", + "onAutoForward": "notify" + }, + "8010": { + "label": "Edit runtime", + "onAutoForward": "silent" + }, + "8020": { + "label": "Display runtime", + "onAutoForward": "silent" + }, + "8030": { + "label": "Server runtime", + "onAutoForward": "silent" + } + }, + "postCreateCommand": "pnpm install --frozen-lockfile && pnpm build && pnpm -C examples/question run build", + "postAttachCommand": "/bin/bash .devcontainer/setup.sh && pnpm -C examples/question run dev", + "customizations": { + "vscode": { + "settings": { + "editor.formatOnSave": true, + "eslint.enable": true, + "prettier.requireConfig": true, + "files.exclude": { + "**/.pnpm": true, + "**/node_modules": true + }, + "workbench.colorTheme": "One Dark Pro", + "oneDarkPro.editorFontLigatures": true, + "oneDarkPro.bold": true, + "oneDarkPro.italic": true, + "editor.fontSize": 20, + "editor.fontFamily": "'Dank Mono', 'Fira Code', monospace", + "editor.fontLigatures": true + }, + "extensions": [ + "akamud.vscode-theme-onedark", + "zhuangtongfa.Material-theme", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "mikestead.dotenv", + "sainoba.px-to-rem", + "stylelint.vscode-stylelint", + "zhuangtongfa.material-theme", + "EditorConfig.EditorConfig", + "Vue.volar", + "GitHub.copilot" + ] + } + } +} diff --git a/example/.devcontainer/setup.sh b/.devcontainer/setup.sh similarity index 100% rename from example/.devcontainer/setup.sh rename to .devcontainer/setup.sh diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 3e775efb..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -auto-install-peers=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1197177c..c87daeb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +### v2.0.0 2026-04-17 + +#### Breaking Changes +- Migrated to Vuetify 4 (MD3 typography classes, updated component API, + theme configuration). All content element packages using Vuetify components + need to be updated. +- Question auto-wrap — `QuestionCard` and `QuestionContainer` are now applied + automatically by the runtime. Elements must remove manual wrapping. +- Build toolchain migrated from tsup to tsdown. +- `isolatedDeclarations` enabled — all exported symbols require explicit type + annotations. +- `StorageApi.upload` now takes a native `File` instead of a `FormData` + payload. The `UploadFormData` and `UploadFormFieldname` type exports have + been removed. +- `mocks` manifest type extracted to a standalone `ElementMocks` interface; + `DisplayContext` relocated to `element-interfaces`. +- TypeScript 6, Vite 8. + +#### Features +- Typed hook signatures (`ElementHook`, `BeforeDisplayHook`, + `OnUserInteractionHook`, `ProcedureHandler`). +- `ServerModule` and `HookMap` types for typed server package default exports. +- `AiConfig` type (replaces inline `OpenAISchema` casting pattern). +- RPC procedures — custom server-side methods callable from Edit components + via the injected `$rpc` function. +- `isEmpty` manifest function for required element validation. +- `showFeedback` manifest field to control question feedback section visibility. +- Question autosave support. +- `mocks.referencesData` manifest field for custom mock linked element data. +- CEK theme testing — ThemeDialog in edit and display runtimes. +- `TailorAssetInput` global component (consolidated from separate upload + components). +- `TailorElementPlaceholder` global component. +- `TailorFileInput` global component for file uploads in the edit runtime. +- New `question` example element alongside `counter`; examples relocated to + `examples/` and `counter` is now the default boot target. +- Expanded E2E testing utilities (`@tailor-cms/cek-e2e`) with API helpers and + additional page object models. +- Accessibility improvements. + +#### Other +- `moduleResolution: "bundler"` across all tsconfigs. +- Bumped all dependencies to the latest versions. + ### v1.0.0 2024-02-07 #### Changes diff --git a/README.md b/README.md index 901dbebd..2b35a127 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,47 @@ -# `xt` Tailor extensions kit +# Content Element Kit + +Development framework for building [Tailor CMS](https://github.com/tailor-cms/author) content elements in isolation. + +Provides a 4-runtime architecture for authoring, displaying, and testing +content elements: + +- **Edit** — Authoring runtime emulating the Tailor CMS environment +- **Display** — End-user runtime emulating the delivery (LMS) environment +- **Server** — Server runtime for hooks, procedures, and storage +- **Preview** — Inspector UI combining all runtimes + +## Packages + +| Package | Description | +|---|---| +| `@tailor-cms/cek-common` | Shared types, API client, WebSocket utils | +| `@tailor-cms/tce-edit-runtime` | Edit runtime | +| `@tailor-cms/tce-display-runtime` | Display runtime | +| `@tailor-cms/tce-server-runtime` | Server runtime | +| `@tailor-cms/tce-preview-runtime` | Preview inspector UI | +| `@tailor-cms/tce-boot` | Dev orchestrator | +| `@tailor-cms/cek-e2e` | E2E testing utilities | +| `@tailor-cms/eslint-config` | Shared ESLint config | + +## Documentation + +See the [documentation site](https://tailor-cms.github.io/xt/) for +installation, usage, and API reference. + +## Quick start + +```bash +pnpm install +pnpm dev +``` + +`pnpm dev` opens an interactive picker for the bundled [examples/](examples/), +then boots the selection across all four runtimes — accessible from the +Preview inspector (default `localhost:8080`). + +To skip the picker, pass the example name directly: + +```bash +pnpm dev counter +``` -Provides base runtime for developing Tailor teaching elements. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index af2f7abe..ae6b1782 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -19,7 +19,7 @@ export default defineConfig({ { text: "Introduction", link: "/introduction" }, { text: "Installation", link: "/installation" }, { text: "Example", link: "/example" }, - { text: "Environment variables", link: "/enviroment-variables" }, + { text: "Environment variables", link: "/environment-variables" }, ], }, { diff --git a/docs/ai.md b/docs/ai.md index 5768734f..5e30f1cc 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -21,9 +21,9 @@ AI configuration is defined by the following: Here is an example configuration for a counter element: ```ts -import { OpenAISchema } from '@tailor-cms/cek-common'; +import type { AiConfig } from '@tailor-cms/cek-common'; -export const ai = { +export const ai: AiConfig = { Schema: { type: 'json_schema', name: 'ce_counter', @@ -36,7 +36,7 @@ export const ai = { required: ['count', 'description'], additionalProperties: false, }, - } as OpenAISchema, + }, getPrompt: () => ` Generate counter content element as an object with the following properties: { "description": "", "count": 0 }. diff --git a/docs/display-package.md b/docs/display-package.md index bd445803..a5a9129e 100644 --- a/docs/display-package.md +++ b/docs/display-package.md @@ -2,7 +2,7 @@ Display package is the subpackage located under `packages/display` exposing `Vue 3` component needed for the Content Element `end-user` rendering. The -component recieves authored values as props. Here is an example of a Display +component receives authored values as props. Here is an example of a Display component for the Simple Counter Content Element from the Edit package section: \ @@ -36,16 +36,15 @@ is a reserved property). In the example above, we use the `userState` prop to display the last time the user has seen the element. In order to achieve that, we need a way to report -user activity from the `Display` component. The specification implements -`interaction` event, which can be used to report user activity to the `end-user` -system. Emitting `interaction` event will result in `onUserInteraction` server -hook being called, where one can implement user state handling and other -interaction-specific handling. For more details on this, visit section -on [user-state hooks](./server-package#user-state-hooks). +user activity from the `Display` component. -\ -Here is a simple example of element submitting interaction event for backend to -process: +### Non-question elements + +Non-question Display components emit the `interaction` event to report user +activity to the end-user system. Emitting `interaction` triggers the +`onUserInteraction` server hook, where you can implement user state handling +and other interaction-specific logic. For more details, visit the section +on [user-state hooks](./server-package#user-state-hooks). \ `Display.vue` @@ -61,15 +60,68 @@ process: import { Element } from 'tce-manifest'; const props = defineProps<{ element: Element; userState: any }>(); -const emit = defineEmits(['interaction']); +const emit = defineEmits<{ interaction: [data: any] }>(); const submit = () => emit('interaction', { myInteractionData: 'example' }); ``` -:::tip State persistance and user event handling +### Question elements + +Question Display components emit `user-input` instead of `interaction`. The +framework auto-wraps question Display components inside a `QuestionForm` +which provides the standard question layout (prompt, hint, submit/retry +controls, feedback). The element only provides the answer-specific UI. + +The `QuestionForm` captures `user-input` events from the inner component, +validates the form on submit, and emits `interaction` to the runtime — which +triggers the `onUserInteraction` server hook. + +\ +`Display.vue` +```vue + + + +``` + +Note: The element does **not** include a submit button — the `QuestionForm` +provides submit/retry controls and manages the submission lifecycle. + +### Event flow summary + +| Element Type | Component Emits | Container | Runtime Receives | +|-------------|----------------|-----------|-----------------| +| Non-question | `interaction` | — | `interaction` → `onUserInteraction` hook | +| Question | `user-input` | QuestionForm → `interaction` | `interaction` → `onUserInteraction` hook | + +:::tip State persistence and user event handling It is up to the `end-user`/`target system` to define mechanisms for end-user -state persistance or any other additional behaviour. +state persistence or any other additional behaviour. ::: ## Composite Elements diff --git a/docs/edit-package.md b/docs/edit-package.md index d56b34d4..9cc946fe 100644 --- a/docs/edit-package.md +++ b/docs/edit-package.md @@ -29,12 +29,16 @@ props: - `:element`: object; Element entity containing all element related data - `:isFocused`: boolean; Is element selected - `:isDragged`: boolean; Is element being dragged; e.g. upon reordering -- `:isReadonly`: boolean; Should element be readonly; e.g. upon copy element seleciton +- `:isReadonly`: boolean; Should element be readonly; e.g. upon copy element selection and observed for element related events: -- `@save` - Emit `data` object to be saved on the `element.data` property. +- `@save` - Emit `data` object to be saved on the `element.data` property + (non-question elements). +- `@update` - Emit partial data to be merged into element state (question + elements only — the QuestionCard wrapper handles persistence). - `@delete` - Delete element (default control already exists) +- `@link` - Open the element linking dialog (see [Linking elements](#linking-elements)) \ As noted above, to store element state, simply emit `save` event passing an @@ -59,10 +63,10 @@ section: +``` + +The `RpcCaller` type is exported from `@tailor-cms/cek-common`. Its +signature is `(procedure: string, payload?: any) => Promise` — the +generic `T` lets you type the return value at each call site. When omitted it +defaults to `any`. The `procedure` parameter maps to a key in the server +package's `procedures` export. For details on defining server procedures, see +the [Server package - Server procedures](/server-package#server-procedures-rpc) +section. + +## Linking elements + +Content elements can reference other elements through the linking mechanism. +When the Edit component emits a `link` event, Tailor CMS opens an element +picker dialog. The selected element's reference is stored on `element.refs`, +and the full resolved element objects are passed via the `references` prop. + +### Emitting the link event + +```vue + + + +``` + +The `references` prop uses the `ElementReferences` type +(`Record[]>`) keyed by the reference name +(e.g. `linked`). Each value is an array of element objects resolved from the +lightweight identifiers stored in `element.refs`. + +### CEK runtime behavior + +In the CEK development environment, emitting `link` opens an informational +dialog and immediately: + +1. Updates `element.refs` on the server with mock reference data + (`{ id, outlineId, containerId }`) +2. Passes a mock element via the `references` prop + +By default, the mock element is auto-generated from `initState`. To provide +custom mock data, define `mocks.referencesData` in the manifest — only the +`data` is needed, the runtime fills in `id`, `type`, etc.: + +```ts +export const mocks = { + displayContexts: [...], + referencesData: { + linked: [{ title: 'Mock linked element', prompt: 'Some prompt' }], + }, +}; +``` + +This lets you develop and test linking UX without running Tailor CMS. + +## When to save the state? Depending on the type of the element, you might wonder what is the best moment to persist element state. Most of the elements are observing isFocused @@ -164,30 +259,127 @@ order to be selected). ## Composite Elements Content elements can be configured as composite elements using the `isComposite` -flag in the manifest. To include a list of composite elements, utilize the +flag in the manifest. To include a list of embedded child elements, utilize the `TailorEmbeddedContainer` global component. Tailor CMS will render the appropriate element list, while the CEK runtime will mock example elements. -The `TailorEmbeddedContainer` component accepts the following props: -- `:container`: object; Data field of the element containing `embeds` in a key-value format. -- `:allowed-elements-config`: array; Array of element configs allowed to be embedded. Usually equals to the `embedElementConfig` prop passed to the Edit package. -- `:isReadonly`: boolean; Indicates if the element should be readonly. Defaults to `false`. -- `:enableAdd`: boolean; Indicates if adding new elements is allowed. Defaults to `true`. -- `:addElementOptions`: object; Additional options passed to the AddElement core component. +See the [Global Components](/global-components#tailorembeddedcontainer) page for +full props, events, and usage examples. ## Question Elements Content elements can be configured as question elements using the `isQuestion` -flag in the manifest. Each question can utilize `QuestionContainer` component -from `@tailor-cms/core-components` package which is used to wrap the component -in the container with the question prompt, content element slot, hint, -optionally feedback and save and cancel actions. If `QuestionContainer` is -used content element must also be configured as `isComposite` in the manifest -because Question prompt is utilizing `TailorEmbeddedContainer` under the hood. - -The `QuestionContainer` component accepts the following props: -- `:elementData`: object; Element entity containing all element related data -- `:showFeedback`: boolean; Controls whether QustionContainer should render -feedback component. -- `:embedElementConfig`: array; array of element configs allowed for the Question prompt -- `:isReadonly`: boolean; Should element be readonly; e.g. upon copy element seleciton +flag in the manifest. Question elements require `isComposite: true` as well, +because the question prompt uses `TailorEmbeddedContainer` under the hood. + +### CEK runtime: QuestionCard + +When `isQuestion` is set, the CEK runtime wraps the Edit component in a +`QuestionCard` — a card UI with a type/icon header, form validation, dirty +tracking, and Save/Cancel controls. + +The `QuestionCard` accepts an `autosave` prop (defaults to `false` for question +elements): + +- **`autosave: false`**: Shows Save/Cancel buttons when the element has unsaved + changes. The form is validated on submit. +- **`autosave: true`**: Changes are persisted immediately on every `@update` + received. The form validates on input. No Save/Cancel buttons are shown. + +The autosave toggle is available in the Settings panel for question elements. + +Unlike regular elements (where `@save` emits the full `data` object and +persists immediately), Edit components inside QuestionCard emit `@update` with +partial data (only the changed fields). The `QuestionCard` merges the update +into its local state and handles persistence via Save/Cancel controls (or +immediately when `autosave` is enabled). + +```vue + +``` + +### QuestionForm (auto-wrapped) + +When `isQuestion: true`, the framework automatically wraps the Edit component +inside a `QuestionForm` — providing the standard question layout: + +1. **Question prompt** — embedded container for question content +2. **Answer UI** — the developer's Edit component (rendered in the default slot) +3. **Hint** — optional hint text field +4. **Feedback** — per-answer feedback fields (controlled by `showFeedback` manifest field) + +The developer's Edit component only needs to render the answer-specific UI. +There is no need to import or use `QuestionForm` manually — the framework +handles the prompt, hint, and feedback sections. + +```vue + + + +``` + +To control whether the feedback section is rendered, set `showFeedback` in +the manifest (defaults to `true`): + +```ts +const manifest: ElementManifest = { + // ... + isQuestion: true, + isComposite: true, + showFeedback: false, // Hide feedback section +}; +``` + +### Question element state shape + +Question elements follow a specific data structure: + +```ts +interface QuestionElementData { + // Array of embed IDs that form the question prompt + question: string[]; + // Key-value map of embedded elements (prompt parts, answer options, etc.) + embeds: Record; + // Whether this question is gradable (has correct answers) + isGradable: boolean; + // Correct answer data (shape varies by question type, present when isGradable) + correct?: any; + // ... additional type-specific fields +} +``` + +### `isGradable` behavior + +The `isGradable` flag controls whether a question element tracks correct +answers: + +- When `isGradable: true` — the element includes a `correct` field in its data + and the Edit UI shows answer correctness controls +- When `isGradable: false` — the `correct` field is removed and the element + functions as a survey/poll question without grading +- The flag can be toggled at runtime via the Settings panel in the CEK, or + via the element configuration in Tailor CMS. Toggling resets the element + data to `initState` with the new `isGradable` value. diff --git a/docs/enviroment-variables.md b/docs/environment-variables.md similarity index 95% rename from docs/enviroment-variables.md rename to docs/environment-variables.md index 7371021a..cb76505c 100644 --- a/docs/enviroment-variables.md +++ b/docs/environment-variables.md @@ -1,10 +1,10 @@ -# Enviroment variables +# Environment variables You can use `.env` configuration file to configure `Content Element Kit` runtime and pass configuration to the content element backend. \ -There are two types of enviroment variables: +There are two types of environment variables: - `Content Element Kit` variables, used to configure service ports, end-user URLs and runtimes. diff --git a/docs/example.md b/docs/example.md index 9db0df74..f73aacf1 100644 --- a/docs/example.md +++ b/docs/example.md @@ -42,11 +42,11 @@ export const name = 'Simple counter'; ``` There is also an `initState` function, which as name implies, initializes state -of the element upon creation. Since we are bulding a simple increment element, +of the element upon creation. Since we are building a simple increment element, we'll define it as: ```ts -export const initState: DataInitializer = (): ElementData => ({ count: 0 }); +export const initState: DataInitializer = (_config) => ({ count: 0 }); ``` We also want to update the type definitions, to do so, open @@ -67,7 +67,7 @@ by displaying updated type and setting the `data.count` value to `0`: Now that we set the basic properties and specified how to initialize the component state, we can create Authoring component for our counter. It is -not going to be the worlds most complex or usefull component; Author will +not going to be the world's most complex or useful component; Author will have an option to click on a button, each time incrementing a counter. Navigate to `packages/edit/src/components/Edit.vue` and paste the following code for our simple counter: @@ -81,7 +81,7 @@ for our simple counter: @@ -203,7 +203,7 @@ defineProps<{ element: Element }>(); ``` \ -Note that Display component recieves spreaded element attributes as props. After +Note that Display component receives element attributes as props. After applying these changes you should be able to see the `Display` component: \ @@ -219,11 +219,14 @@ to enable access). In this example, we are going to reset the counter in case it reaches 10. -Navigate to the `packages/server/src/index.ts` and update beforeSave function +Navigate to the `packages/server/src/index.ts` and update the `beforeSave` hook to: ```ts -export function beforeSave(element: Element, services: any) { +import type { ElementHook } from '@tailor-cms/cek-common'; +import type { Element } from 'tce-manifest'; + +export const beforeSave: ElementHook = (element) => { if (element.data.count >= 10) { element.data = { ...element.data, @@ -231,7 +234,7 @@ export function beforeSave(element: Element, services: any) { }; } return element; -} +}; ``` \ @@ -252,7 +255,7 @@ In case that hook code changes are not picked up automatically, please restart. ## Conclusion -Congradulations! You have created your first Content Element. For more details +Congratulations! You have created your first Content Element. For more details on each of the components please visit the matching documentation section. For a fully working example, please visit https://github.com/tailor-cms/tce-counter. diff --git a/docs/file-storage.md b/docs/file-storage.md index 37063c3f..64972b4e 100644 --- a/docs/file-storage.md +++ b/docs/file-storage.md @@ -4,52 +4,44 @@ Available in version >=0.1.0 ::: -## File upload from authoring package +## TailorFileInput component -The authoring components (Edit package) have the `$storageService` provided via -Vue [provide/inject](https://v2.vuejs.org/v2/api/#provide-inject) prop-drilling -feature. To upload a file simpliy inject `$storageService` into your component -and pass `FormData` with the `file` property containing the upload data -([File type](https://developer.mozilla.org/en-US/docs/Web/API/File])) -to the `$storageService.upload` method: +Use the globally available `TailorFileInput` component for file upload and +URL import. It provides a button-mode picker with drag & drop upload dialog, +optional URL import tab, and auto-detection of asset type from extensions. -```ts +See the [Global Components](/global-components#tailorfileinput) page for full +props, events, and usage examples. + +The legacy `TailorAssetInput` is still available for backward compatibility. + +## Direct file upload + +For custom upload UI, inject `$storageService` and pass an array of +[File](https://developer.mozilla.org/en-US/docs/Web/API/File) objects to the +`upload` method: + +```vue ``` -There are a few things to note for the example above. We used `createUploadForm` -helper which constructs `FormData` payload (based on the file input change -event). After we upload the asset, we recieve: +After we upload the assets, we receive: - `key`; image storage key - `url`; internal url, used to identify Tailor managed static assets @@ -71,12 +61,12 @@ event). After we upload the asset, we recieve: Since `publicUrl` is going to expire at some point (with the production provider), there needs to be a mechanism in place which will make sure to process all static assets upon need. As mentioned in the -[State section](http://localhost:5173/xt/state.html#data-assets-property) +[State section](/state#data-assets-property) there is a special `data.assets` property, where all static assets handled by the `Content Element` need to be declared. In the example above, we assign internal url value to `assets.backgroundUrl`. Once fetched for delivery, default asset processing will make sure to assign resolved public -`backgroundUrl` to the `element.data` property. The same mechanim needs to be +`backgroundUrl` to the `element.data` property. The same mechanism needs to be implemented by the consumer of the `Display package`. The `key` of the asset declared within the `assets` object should be set to the @@ -87,37 +77,30 @@ declaring `x.y` key will result with the resolved url assigned to the ## Server hooks Server hooks have the `storage` service injected, exposing the ability to access -storage provider methods. The server hooks are defined as: +storage provider methods. Here is an example of retrieving a public url for a +specific element key within a server hook: ```ts -function hook(element: SequelizeModel, services: Object) => element -``` +import type { ElementHook } from '@tailor-cms/cek-common'; +import type { Element } from 'tce-manifest'; -with the `services` object containing `storage` property (injected -storage service). Here is an example of retrieving a public url for a specific -element key within the server hook: - -```ts -async function afterSave(element: SequelizeModel, services: Object) => { +export const afterSave: ElementHook = async (element, services) => { const { storage } = services; const publicUrl = await storage.getFileUrl(element.assets.myKey); -} + return element; +}; ``` -At the moment it is possible to: - -```ts -getPath(...segments: string[]) -``` +Available storage methods: ```ts -getFile(key: string) +getFile(key: string): Promise ``` ```ts -getFileUrl(key: string) +getFileUrl(key: string): Promise ``` ```ts -saveFile(key: string, data: string | NodeJS.ArrayBufferView | Iterable | AsyncIterable | internal.Stream) +saveFile(key: string, data: string | Buffer | DataView): Promise ``` diff --git a/docs/global-components.md b/docs/global-components.md new file mode 100644 index 00000000..d79b0666 --- /dev/null +++ b/docs/global-components.md @@ -0,0 +1,227 @@ +# Global Components + +The following Vue components are globally registered and available without +importing them. + +Each runtime (edit and display) registers its own set of global components. +In the CEK, these are mock implementations for local development (e.g. +`TailorEmbeddedContainer` renders example elements, `TailorAssetInput` uploads +to the local dev server). In production, the host application (Tailor CMS for +authoring, the LMS for display) registers its own implementations that connect +to the real storage, element registry, and other platform services. The API +(props and events) is the same across environments. + +## Edit Runtime + +Registered by the CEK edit runtime and Tailor CMS. Available in Edit, +TopToolbar, and SideToolbar components. + +### TailorFileInput + +File picker component with upload dialog (drag & drop) and optional URL +import tab. Auto-detects asset type from extensions to resolve icon, label, +and button text. Handles file upload via `$storageService`. + +```vue + + + +``` + +#### Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `file-key` | `string` | `''` | Storage key or `storage://` URI of the current file | +| `file-name` | `string` | `''` | Display name; falls back to parsing from `fileKey` | +| `allow-url-source` | `boolean` | `false` | Show URL import tab in the picker dialog | +| `allowed-extensions` | `string[]` | `[]` | Accepted extensions with dot prefix (e.g. `['.jpg', '.png']`); drives icon/label auto-detection | +| `use-field-input` | `boolean` | `false` | Use field input + card rendering instead of default button mode | +| `show-preview` | `boolean` | `false` | Enable image thumbnail + overlay on the file card; auto-enabled for image extensions | +| `public-url` | `string \| null` | `null` | Pre-resolved public URL; skips async fetch when present | +| `label` | `string` | `''` | Override auto-inferred label (derived from extensions) | +| `placeholder` | `string` | `''` | Override button text (e.g. `'Upload image'`) | +| `icon` | `string` | `''` | Override auto-inferred icon (derived from extensions) | +| `variant` | `VTextField['variant']` | `'outlined'` | Vuetify variant for the text field (field input mode) | +| `density` | `VTextField['density']` | `'default'` | Vuetify density for the text field (field input mode) | +| `dark` | `boolean` | `false` | Dark theme variant for the file preview card | + +#### Events + +| Event | Payload | Description | +|---|---|---| +| `@upload` | `{ key, name, url, publicUrl }` | File uploaded via drag & drop or file picker | +| `@input` | `{ url, publicUrl, title? } \| null` | URL imported (from URL tab), or `null` on clear | +| `@delete` | — | File cleared by the user | + +Auto-inferred values from `allowedExtensions`: +- Image extensions → icon `mdi-image-outline`, label `Image`, button `Choose image` +- Video → `mdi-video-outline` / `Video` / `Choose video` +- Audio → `mdi-volume-medium` / `Audio` / `Choose audio` +- Document → `mdi-file-document-outline` / `Document` / `Choose document` +- Fallback → `mdi-file` / `File` / `Choose file` + +### TailorAssetInput (legacy) + +The previous asset input component with inline edit/save/cancel state. +Still registered globally for backward compatibility. New elements should +use `TailorFileInput` instead. + +See the [File storage](/file-storage) section for details on asset URL handling. + +### TailorElementPlaceholder + +Placeholder component shown when no content has been provided yet (e.g. no +image uploaded, no URL set). Displays a centered icon, name, and contextual +instructions that change based on focus state. + +```vue + +``` + +#### Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `icon` | `string` | required | MDI icon name | +| `name` | `string` | required | Element display name | +| `placeholder` | `string` | `'Select to edit'` | Text shown when unfocused | +| `active-placeholder` | `string` | `'Use toolbar to edit'` | Text shown when focused | +| `active-icon` | `string \| null` | `null` | Icon shown next to active placeholder | +| `active-color` | `string` | `'#fff'` | Icon color when focused | +| `dense` | `boolean` | `false` | Compact variant (smaller icon/text) | +| `is-focused` | `boolean` | `false` | Focus state | +| `is-disabled` | `boolean` | `false` | Disabled state (greys out icon and text) | + +### TailorEmbeddedContainer + +Interactive container for embedded child elements within composite elements. +Supports adding, editing, deleting, and reordering embedded elements. + +```vue + +``` + +#### Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `container` | `object` | required | Data field containing `embeds` in key-value format | +| `allowed-elements-config` | `array` | `[]` | Element configs allowed to be embedded | +| `is-readonly` | `boolean` | `false` | Disable editing | +| `enable-add` | `boolean` | `true` | Show add element button | +| `add-element-options` | `object` | see below | Options for the add element button | + +Default `addElementOptions`: +```ts +{ + large: false, + label: 'Add content', + icon: 'mdi-plus', + color: 'primary-darken-4', + variant: 'tonal', +} +``` + +#### Events + +| Event | Payload | Description | +|---|---|---| +| `@save` | `object` | Updated container object with modified embeds | +| `@delete` | `object` | Element to be deleted | + +See [Composite Elements](/edit-package#composite-elements) for usage details. + +### TailorContentElement + +::: warning Internal +Used internally by `TailorEmbeddedContainer` to render individual embedded +elements. Not intended for direct use by element authors. +::: + +Renders a single embedded element with edit controls (text input, delete +button on hover). + +#### Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `element` | `object` | required | Element entity | +| `parent` | `object \| null` | `null` | Parent element (hides delete when set) | +| `is-readonly` | `boolean` | `false` | Disable editing | + +#### Events + +| Event | Payload | Description | +|---|---|---| +| `@save` | `object` | Updated element data | +| `@delete` | `object` | Element to be deleted | + +## Display Runtime + +Registered by the CEK display runtime. In production, the LMS that consumes +the Display package is responsible for registering these components. + +### TailorEmbeddedContainer + +Read-only container that renders embedded child elements for display. + +```vue + +``` + +#### Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `elements` | `array` | required | Array of embedded element objects to render | \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index c54d219a..c98d0a6e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,9 +5,9 @@ Here you will find information on setting up and running the Content Element Kit ## Prerequisites - macOS or Linux -- [node](https://nodejs.org/en) - We recommend you have either 16.x or 20.x +- [Node.js](https://nodejs.org/en) - We recommend you have the latest LTS version installed. -- [pnpm](https://pnpm.io/installation) version 8.x. +- [pnpm](https://pnpm.io/installation) - git ## Setup diff --git a/docs/manifest.md b/docs/manifest.md index 205babf8..517edc95 100644 --- a/docs/manifest.md +++ b/docs/manifest.md @@ -27,10 +27,17 @@ export interface ElementManifest { // type is gradable or ungradable. If both are supported, this field // should be omitted. isGradable?: boolean; - // The goal of the initState function is to properly initialize the - // 'data' field upon the Content Element creation. The 'data' field is - // the Content Element property storing authors input. + // Controls whether the QuestionForm renders the feedback section. + // Only relevant when 'isQuestion' is true. Defaults to true. + showFeedback?: boolean; + // Initializes the 'data' field upon Content Element creation. + // Receives an optional config object with runtime-level settings + // (e.g. { isGradable }) that may influence the initial data shape. initState: DataInitializer; + // Optional function to determine if element data is considered empty. + // Used by the authoring system to evaluate required content elements. + // Receives current element data and returns true if empty. + isEmpty?: (data: TData) => boolean; // Edit component of the Content Element (Used for authoring purposes). Edit?: object; // TopToolbar component of the Content Element Edit component @@ -51,21 +58,10 @@ export interface ElementManifest { forceFullWidth: boolean; }, // AI tools configuration. - ai?: { - // Prompt used to describe the response structure. - getPrompt: (context: any) => string; - // JSON schema for the OpenAI response formatting. - Schema?: OpenAISchema; - // Function for additional response processing & validation. - processResponse?: (val: any) => any; - // Indicates whether the AI generation tool should be used when - // generating. - useImageGenerationTool?: boolean; - }; - mocks?: { - // Provide end-user system context mock (used for user state hooks) - // See https://tailor-cms.github.io/xt/server-package.html#user-state-hooks. - displayContexts: Array<{ name: string; data: any }>; - }; + // See [AI page](/ai.html) and AiConfig interface for details. + ai?: AiConfig; + // CEK development mocks (display context presets, link dialog mock data). + // See `ElementMocks` interface for details. + mocks?: ElementMocks; } ``` diff --git a/docs/package.json b/docs/package.json index 62352c8d..5ce16f4e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,11 +1,12 @@ { "name": "content-element-kit-docs", + "private": true, "scripts": { "docs:dev": "vitepress dev", "docs:build": "vitepress build", "docs:preview": "vitepress preview" }, "devDependencies": { - "vitepress": "1.6.3" + "vitepress": "1.6.4" } } diff --git a/docs/runtime.md b/docs/runtime.md index 76228aa8..e1552e43 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -1,10 +1,10 @@ # Runtime -The framework consist of four different runtimes: +The framework consists of four different runtimes: - Authoring; autoloading and serving `edit` package. This runtime - supports Vue 3 based UI componenets and implements compatible Tailor CMS APIs - to enable development without the need to set up Talor CMS. By default it runs + supports Vue 3 based UI components and implements compatible Tailor CMS APIs + to enable development without the need to set up Tailor CMS. By default it runs on port `8010`. - Display; autoloading and serving `display` package. This runtime supports Vue 3 and implements general LMS front-end interface. Runs on port diff --git a/docs/server-package.md b/docs/server-package.md index b40a8dd9..006d2151 100644 --- a/docs/server-package.md +++ b/docs/server-package.md @@ -22,11 +22,13 @@ Fetch element lifecycle: All hooks are called with 2 arguments. The first argument is the element itself, and the second argument is the service bag which contains the config service -and the storage service (these services have not yet been ported into the -Content Element Kit). Hook declaration looks as follows: +and the storage service. Hook declaration looks as follows: ```ts -function hook(element: SequelizeModel, services: Object) => element +import type { ElementHook } from '@tailor-cms/cek-common'; +import type { Element } from 'tce-manifest'; + +const hook: ElementHook = (element, services) => element; ``` The hook function returns the original or modified element. For example, @@ -38,15 +40,15 @@ In the example section, we created a simple hook for our Counter element, which resets the counter value if it reaches 10. ```ts -export function beforeSave(element, services) { +import type { ElementHook } from '@tailor-cms/cek-common'; +import type { Element } from 'tce-manifest'; + +export const beforeSave: ElementHook = (element) => { if (element.data.count >= 10) { - element.data = { - ...element.data, - count: 0, - }; + element.data = { ...element.data, count: 0 }; } return element; -} +}; ``` \ Hooks also enable interfacing with external libraries, server side validation @@ -58,6 +60,85 @@ All changes made by hooks are automatically propagated to the authoring front-end using SSE (Server Side Events). :::: +## Server procedures (RPC) + +Server procedures allow content elements to define custom server-side methods +that can be called from Edit, TopToolbar, or SideToolbar components with +request/response semantics. This is useful for operations that need to run on +the server — such as calling external APIs, processing data, or performing +secure operations. + +### Defining procedures + +Export a `procedures` object from your server package. Each key is a procedure +name and each value is a handler function: + +```ts +import type { ProcedureHandler, ServerModule } from '@tailor-cms/cek-common'; + +export const procedures: Record = { + async generateSummary(services, payload) { + // services - { config, storage } (same as hooks) + // payload - data sent from the frontend + const { prompt } = payload; + const summary = await someExternalApi.generate(prompt); + return { summary }; + }, + getStats(services, payload) { + return { + wordCount: payload.content?.split(' ').length ?? 0, + }; + }, +}; + +const serverModule: ServerModule = { + type, + initState, + hookMap, + procedures, + // ...hooks +}; + +export default serverModule; +``` + +Handler signature: `ProcedureHandler` — `(services, payload) => Promise | any` + +The `services` object contains the same `config` and `storage` services +available in hooks. Procedures are self-contained — any element data needed +should be passed by the frontend via `payload`. This means procedures work +for both top-level and embedded elements without special routing. + +### Calling from Edit components + +Procedures are called via the injected `$rpc` function: + +```ts +import type { RpcCaller } from '@tailor-cms/cek-common'; + +const rpc = inject('$rpc') as RpcCaller; + +// Call with payload (typed return) +const { summary } = await rpc<{ summary: string }>('generateSummary', { + prompt: 'Summarize this element', +}); + +// Pass element data when the procedure needs it +const stats = await rpc('getStats', { + content: element.data.content, +}); +``` + +The function returns a Promise that resolves with the handler's return value. +See [Edit package - Calling server procedures](/edit-package#calling-server-procedures) +for more on typing. + +### Route + +Each procedure maps to: `POST /content-element/rpc/:procedureName` + +If the procedure doesn't exist, a `404` response is returned. + ## User state hooks ::::tip ☝️ Note @@ -70,27 +151,31 @@ user-specific state of a particular element. There are two state hooks available - `onUserInteraction` The `end-user` system has the full flexibility for implementing the state -management and persistance. User-state hooks recieve `displaySystemContext`, +management and persistence. User-state hooks receive `displaySystemContext`, which can be provided by the `end-user` system and used to add additional context needed for the state resolution. For content element kit this is mocked within the element manifest and injected into previously listed hooks. ```ts -mocks?: { - displayContexts: Array<{ name: string; data: any }>; +// In your manifest: +export const mocks: ElementMocks = { + displayContexts: [ + { name: 'Default', data: {} }, + { name: 'Interacted', data: { visited: true } }, + ], }; ``` ::::tip ☝️ Note Only the first value is injected at the moment. In the future versions, the -system will ofer a dropdown to select the display context mock, in case one +system will offer a dropdown to select the display context mock, in case one wants to quickly mock different user states and switch between them. :::: ### `beforeDisplay` hook `beforeDisplay` hook is responsible for resolving a user state of a particular -content element (binded as `userState` upon rendering the `Display` component). -The hook recieves authored content `element`, the end-user system +content element (bound as `userState` upon rendering the `Display` component). +The hook receives authored content `element`, the end-user system `displaySystemContext` and returns a resolved user state. ```ts @@ -101,8 +186,8 @@ function beforeDisplay( ### `onUserInteraction` hook -`onUserInteraction` hook is triggered when the `Display` components `emits` the -`@interaction` event. The hook recieves authored content `element`, the target +`onUserInteraction` hook is triggered when the `Display` component emits the +`@interaction` event. The hook receives authored content `element`, the target system `displaySystemContext` and the payload emitted by the `Display` component. ```ts @@ -123,9 +208,9 @@ containing the data (alongside `updateDisplayState` flag). This will be injected into the `displaySystemContext` for you upon the `beforeDisplay` hook call (`tce-boot` >= `0.2.1`). -### Mocking `end-user` system state persistance and handling +### Mocking `end-user` system state persistence and handling -In addition to the context mock, one might want to mimick the persistance and +In addition to the context mock, one might want to mimic the persistence and context handling mechanism. At the moment, it is possible to rely on the `CEK_RUNTIME` env variable to detect if the hook is running within the development runtime and inject any arbitrary development specific code diff --git a/docs/state.md b/docs/state.md index 5e3f1f04..eb70a3b8 100644 --- a/docs/state.md +++ b/docs/state.md @@ -4,7 +4,7 @@ Content Element `Edit` component has the ability to emit the `data` object that needs to be stored by the `Tailor CMS`; which is later on used to render -`Display` component. This is achived by emitting the `save` event. +`Display` component. This is achieved by emitting the `save` event. The emitted `data` is stored on the `ContentElement` entity under the same key. Here is a Content Element `Edit` Counter component from the Example section, displaying the amount of times user clicked on a button: @@ -18,10 +18,10 @@ section, displaying the amount of times user clicked on a button: ``` +Question elements use the `@update` event instead of `@save`. The emitted data +can be partial (only the changed fields). The `QuestionCard` merges the update +into its local state and handles persistence based on the `autosave` +configuration. See the [Edit package](/edit-package#question-elements) docs +for more details. + ## Initializing the state By emitting the `save` event, `count` value is stored alongside other @@ -94,7 +100,7 @@ specification, please visit dedicated sections within the documentation. Content Element Kit fully replicates `Tailor CMS` Content Element model to ensure consistency and compatibility. The same `ORM` is used to instantiate -entities on the backend (`Sequalize.js`), and compatible mechanism is +entities on the backend (`Sequelize.js`), and compatible mechanism is implemented to hydrate front-end component in case of changes. ```ts @@ -159,4 +165,4 @@ interface ContentElement { ``` Full Content Element entity ORM instance is available within server-side hooks. -For more info on that. Please visit `Server package` section. +For more info, please visit the `Server package` section. diff --git a/docs/testing.md b/docs/testing.md index 1440820c..de4f5124 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,14 +1,119 @@ # Testing -The framework includes `playwright` setup, located under `/test` directory. -To run tests, simply execute: +The framework includes [Playwright](https://playwright.dev) E2E testing setup, +located under the `/test` directory. + +## Running Tests ```bash pnpm test ``` -from the project root. To open up UI preview, pass the `--ui` flag: +To open the interactive UI mode: ```bash pnpm test -- --ui ``` + +## E2E Package + +The `@tailor-cms/cek-e2e` package provides shared testing utilities for content +elements. It includes Page Object Models (POMs) for the CEK preview panels and +an API client for interacting with the server runtime. + +### Page Object Models + +Import POMs via the `pom` namespace: + +```ts +import { pom } from '@tailor-cms/cek-e2e'; +``` + +| POM | Description | +| ---------------------- | ----------------------------------------------------- | +| `EditPanel` | Edit iframe — editor, toolbars, settings, theme dialog | +| `DisplayPanel` | Display iframe — editor, state preset, theme dialog | +| `BottomPanel` | Bottom panel — authoring & user state history | +| `ThemeDialog` | Theme selector — select, add, edit, remove themes | +| `EditQuestionForm` | Edit question form — prompt, hint, feedback, save | +| `DisplayQuestionForm` | Display question form — hint, submit, retry, feedback | +| `FileInput` | File input — upload, URL import, download, preview | + +Extend the base POMs to add element-specific locators: + +```ts +import type { Locator, Page } from '@playwright/test'; +import { pom } from '@tailor-cms/cek-e2e'; + +export class Edit extends pom.EditPanel { + readonly incrementBtn: Locator; + + constructor(page: Page) { + super(page); + this.incrementBtn = this.editor.getByRole('button', { name: 'Increment' }); + } +} + +### Element Client + +The `elementClient` is a pre-configured singleton for interacting with the +content element server runtime API: + +```ts +import { elementClient } from '@tailor-cms/cek-e2e'; + +// Get element data +await elementClient.get(id); + +// Update element data +await elementClient.update(id, { count: 5 }); + +// Reset element to initial state +await elementClient.reset(id); + +// Set user state context by index +await elementClient.setState(id, 1); + +// Reset user state context +await elementClient.resetState(id); +``` + +## Test Structure + +A typical test directory looks like: + +``` +test/ +├── playwright.config.ts +├── pom/ +│ ├── index.ts +│ ├── Display.ts # extends DisplayPanel +│ └── Edit.ts # extends EditPanel +└── spec/ + ├── display.spec.ts + └── edit.spec.ts +``` + +## Writing Tests + +A basic test file: + +```ts +import { elementClient } from '@tailor-cms/cek-e2e'; +import { expect, test } from '@playwright/test'; + +import { Edit } from '../pom'; + +const ELEMENT_ID = 'test-element-id'; + +test.beforeEach(async ({ page }) => { + await elementClient.reset(ELEMENT_ID); + await page.goto(`/?id=${ELEMENT_ID}`); + await page.waitForLoadState('networkidle'); +}); + +test('Renders edit component', async ({ page }) => { + const edit = new Edit(page); + await expect(edit.editor).toBeVisible(); +}); +``` diff --git a/docs/theming.md b/docs/theming.md new file mode 100644 index 00000000..ea6c98f9 --- /dev/null +++ b/docs/theming.md @@ -0,0 +1,51 @@ +# Theme Testing + +The edit package targets Tailor CMS and inherits its Vuetify theme, while the +display package runs inside the target LMS with its own theme. The **Theme +selector** (palette icon in the toolbar) lets you preview how your element looks +under different themes during development. + +## Built-in Themes + +| Runtime | Default themes | +| ---------- | --------------------------- | +| Authoring | Tailor (Tailor CMS palette) | +| Display | Light, Dark | + +## Custom Themes + +To test against a specific LMS or Tailor CMS theme, click the palette icon and +select **Add custom theme**. Provide a name and paste a Vuetify +[ThemeDefinition](https://vuetifyjs.com/en/features/theme/#theme-object-structure): + +```js +{ + dark: true, + colors: { + primary: '#9BCBFB', + secondary: '#83D5C6', + background: '#0D1B2A', + surface: '#1B2838', + error: '#FFB4AB', + success: '#6EE7B7', + warning: '#FCD34D', + info: '#7DD3FC', + }, +} +``` + +The input supports JSON5 — unquoted keys, single quotes, trailing commas, and +comments are all accepted. This makes it easy to paste theme definitions directly +from your LMS or Tailor CMS codebase. + +## Color Preview + +When a theme is selected, the dialog displays all available color names +(e.g. `primary`, `surface`, `error`). These are the values you can pass to +Vuetify component `color` props. Use this to discover which theme variables are +available and verify they look correct before using them in your element. + +::: tip +Custom themes persist in `localStorage` across sessions and can be edited or +removed from the theme selector dialog. +::: diff --git a/example/.env b/example/.env deleted file mode 100644 index 3da843d1..00000000 --- a/example/.env +++ /dev/null @@ -1,22 +0,0 @@ -# Service ports -PREVIEW_RUNTIME_PORT=8001 -EDIT_RUNTIME_PORT=8002 -DISPLAY_RUNTIME_PORT=8003 -SERVER_RUNTIME_PORT=8004 - -# External urls -PREVIEW_RUNTIME_URL=http://localhost:8001 -EDIT_RUNTIME_URL=http://localhost:8002 -DISPLAY_RUNTIME_URL=http://localhost:8003 -SERVER_RUNTIME_URL=http://localhost:8004 - -# AI service configuration -# If AI_UI_ENABLED is set to true, the AI service will be enabled and model id -# and secret key must be provided. -AI_UI_ENABLED= -AI_MODEL_ID= -AI_SECRET_KEY= - -# Content Element env variables; TCE_ prefix is required -# Will be loaded to the server runtime -TCE_TEST=123 diff --git a/example/.env.example b/example/.env.example deleted file mode 100644 index 3da843d1..00000000 --- a/example/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -# Service ports -PREVIEW_RUNTIME_PORT=8001 -EDIT_RUNTIME_PORT=8002 -DISPLAY_RUNTIME_PORT=8003 -SERVER_RUNTIME_PORT=8004 - -# External urls -PREVIEW_RUNTIME_URL=http://localhost:8001 -EDIT_RUNTIME_URL=http://localhost:8002 -DISPLAY_RUNTIME_URL=http://localhost:8003 -SERVER_RUNTIME_URL=http://localhost:8004 - -# AI service configuration -# If AI_UI_ENABLED is set to true, the AI service will be enabled and model id -# and secret key must be provided. -AI_UI_ENABLED= -AI_MODEL_ID= -AI_SECRET_KEY= - -# Content Element env variables; TCE_ prefix is required -# Will be loaded to the server runtime -TCE_TEST=123 diff --git a/example/.npmrc b/example/.npmrc deleted file mode 100644 index 3e775efb..00000000 --- a/example/.npmrc +++ /dev/null @@ -1 +0,0 @@ -auto-install-peers=true diff --git a/example/packages/display/.gitignore b/example/packages/display/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/example/packages/display/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/example/packages/display/README.md b/example/packages/display/README.md deleted file mode 100644 index 8b1397cd..00000000 --- a/example/packages/display/README.md +++ /dev/null @@ -1 +0,0 @@ -# Content element `Display` component diff --git a/example/packages/display/src/components/Display.vue b/example/packages/display/src/components/Display.vue deleted file mode 100644 index 6091571b..00000000 --- a/example/packages/display/src/components/Display.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/example/packages/edit/.gitignore b/example/packages/edit/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/example/packages/edit/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/example/packages/edit/README.md b/example/packages/edit/README.md deleted file mode 100644 index c7738ce3..00000000 --- a/example/packages/edit/README.md +++ /dev/null @@ -1 +0,0 @@ -# Content element `Edit` component diff --git a/example/packages/manifest/README.md b/example/packages/manifest/README.md deleted file mode 100644 index e02fded4..00000000 --- a/example/packages/manifest/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Content element manifest - -Exposes shared element definition diff --git a/example/packages/server/.gitignore b/example/packages/server/.gitignore deleted file mode 100644 index 903de499..00000000 --- a/example/packages/server/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -node_modules/ -/dist/ - -# OS -.DS_Store - -# Local env files -.env -.env.local -.env.*.local - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw* diff --git a/example/packages/server/src/index.ts b/example/packages/server/src/index.ts deleted file mode 100644 index c332f331..00000000 --- a/example/packages/server/src/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ai, initState, mocks, type } from 'tce-manifest'; -import type { HookServices, ServerRuntime } from '@tailor-cms/cek-common'; -import type { Element } from 'tce-manifest'; - -const userStateMock: any = {}; - -export function beforeSave(element: Element, _services: HookServices) { - if (element.data.count >= 10) { - element.data = { - ...element.data, - count: 0, - }; - } - - return element; -} - -export function afterSave(element: Element, _services: HookServices) { - console.log('After save hook'); - return element; -} - -export function afterLoaded( - element: Element, - _services: HookServices, - runtime: ServerRuntime, -) { - console.log('After loaded hook', runtime); - return element; -} - -export function afterRetrieve( - element: Element, - _services: HookServices, - runtime: ServerRuntime, -) { - console.log('After retrieve hook', runtime); - return element; -} - -export function beforeDisplay(_element: Element, context: any) { - console.log('beforeDisplay hook'); - console.log('beforeDisplay context', context); - return { ...context, ...userStateMock }; -} - -export function onUserInteraction( - _element: Element, - context: any, - _payload: any, -): any { - // Can have arbitrary return value - // displayState is passed to the client if defined - userStateMock.interactionTimestamp = new Date().getTime(); - context.contextTimestamp = userStateMock.interactionTimestamp; - return { updateDisplayState: true }; -} - -export const hookMap = new Map( - Object.entries({ - beforeSave, - afterSave, - afterLoaded, - afterRetrieve, - onUserInteraction, - beforeDisplay, - }), -); - -export default { - type, - initState, - hookMap, - beforeSave, - afterSave, - afterLoaded, - afterRetrieve, - onUserInteraction, - beforeDisplay, - mocks, - ai, -}; - -export { type, initState, mocks, ai }; diff --git a/example/.editorconfig b/examples/counter/.editorconfig similarity index 100% rename from example/.editorconfig rename to examples/counter/.editorconfig diff --git a/examples/counter/.env.example b/examples/counter/.env.example new file mode 100644 index 00000000..4545e4d9 --- /dev/null +++ b/examples/counter/.env.example @@ -0,0 +1,18 @@ +# Service ports +PREVIEW_RUNTIME_PORT=8080 +EDIT_RUNTIME_PORT=8010 +DISPLAY_RUNTIME_PORT=8020 +SERVER_RUNTIME_PORT=8030 + +# External urls +PREVIEW_RUNTIME_URL=http://localhost:8080 +EDIT_RUNTIME_URL=http://localhost:8010 +DISPLAY_RUNTIME_URL=http://localhost:8020 +SERVER_RUNTIME_URL=http://localhost:8030 + +# AI service configuration +# If AI_UI_ENABLED is set to true, the AI service will be enabled and model id +# and secret key must be provided. +AI_UI_ENABLED= +AI_MODEL_ID= +AI_SECRET_KEY= diff --git a/example/.gitignore b/examples/counter/.gitignore similarity index 68% rename from example/.gitignore rename to examples/counter/.gitignore index f9e70db7..65c87891 100644 --- a/example/.gitignore +++ b/examples/counter/.gitignore @@ -5,7 +5,7 @@ node_modules/ .DS_Store # Local env files -!.env +.env .env.local .env.*.local !.env.example @@ -23,3 +23,10 @@ yarn-error.log* *.njsproj *.sln *.sw* + +# Playwright +playwright-report +test/out +test/test-results/ +test/playwright/.cache/ +test/.boot-state.json diff --git a/examples/counter/README.md b/examples/counter/README.md new file mode 100644 index 00000000..167f9824 --- /dev/null +++ b/examples/counter/README.md @@ -0,0 +1,43 @@ +# Simple Counter + +Click counter with an author-set description and an optional background +image. The author increments and decrements the count, optionally uploads +a background image, and can link to another element. The end user can +submit interactions back to the server. Counts above 9 reset to 0 on save. + +**Type:** `ACME_TCE_COUNTER` + +## Data + +| Field | Type | Description | +|-------|------|-------------| +| `count` | `number` | The counter value | +| `description` | `string` | Author-provided label | +| `key` | `string?` | Storage key for the uploaded background | +| `assets` | `{ backgroundUrl: string }?` | Internal storage URLs (dot-notation) | +| `backgroundUrl` | `string?` | Public URL of the uploaded background | + +`data.assets.*` holds storage keys, `data.backgroundUrl` holds the resolved +public URL. See [docs/file-storage.md](../../docs/file-storage.md). + +## Edit + +- Description text field +- Increment button (decrement lives in the top toolbar) +- Background image upload with preview +- Link element button (renders the linked element's data when present) +- Export data button — downloads the element's data as JSON via a server procedure + +## Display + +- Shows the description and current count +- Submit interaction button — records a server-side timestamp +- Shows the current user state as JSON for debugging + +## Development + +```sh +pnpm dev # Preview :8080 | Edit :8010 | Display :8020 | Server :8030 +pnpm build +pnpm lint +``` diff --git a/examples/counter/package.json b/examples/counter/package.json new file mode 100644 index 00000000..feff1144 --- /dev/null +++ b/examples/counter/package.json @@ -0,0 +1,38 @@ +{ + "name": "tce-counter", + "description": "Counter content element example", + "version": "0.0.1", + "author": "Studion ", + "type": "module", + "private": true, + "exports": { + "./edit": { + "import": "./packages/edit/dist/index.js", + "require": "./packages/edit/dist/index.cjs" + }, + "./display": { + "import": "./packages/display/dist/index.js", + "require": "./packages/display/dist/index.cjs" + }, + "./server": "./packages/server/dist/index.js" + }, + "scripts": { + "dev": "pnpm boot:cek", + "boot:cek": "cd ./node_modules/@tailor-cms/tce-boot && pnpm start --default-display", + "build": "pnpm -r --filter=./** run build", + "lint": "pnpm -r --filter=./** run lint", + "lint:fix": "pnpm -r --filter=./** run lint --fix", + "nuke:dist": "pnpm -r --filter=./** run nuke:dist", + "nuke": "pnpm -r --filter=./** run nuke && pnpm dlx del-cli node_modules", + "test": "playwright test --config test/playwright.config.ts" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@tailor-cms/cek-e2e": "workspace:*", + "@tailor-cms/eslint-config": "workspace:*", + "@tailor-cms/tce-boot": "workspace:*", + "@types/node": "^24.12.2", + "dotenv": "^17.4.2", + "typescript": "^6.0.3" + } +} diff --git a/example/packages/display/eslint.config.js b/examples/counter/packages/display/eslint.config.js similarity index 100% rename from example/packages/display/eslint.config.js rename to examples/counter/packages/display/eslint.config.js diff --git a/example/packages/display/package.json b/examples/counter/packages/display/package.json similarity index 63% rename from example/packages/display/package.json rename to examples/counter/packages/display/package.json index 3a088cfa..8547b9ae 100644 --- a/example/packages/display/package.json +++ b/examples/counter/packages/display/package.json @@ -1,8 +1,10 @@ { - "name": "tce-display", - "private": true, - "type": "module", + "name": "tce-counter-display", + "description": "Counter element display component", "version": "0.0.1", + "author": "Studion ", + "type": "module", + "private": true, "exports": { ".": { "import": "./dist/index.js", @@ -24,18 +26,16 @@ "prepublish": "pnpm build" }, "peerDependencies": { - "vue": "^3.5.13" + "vue": "^3.5.32" }, "devDependencies": { "@tailor-cms/eslint-config": "workspace:*", - "@types/stringify-object": "^4.0.5", - "@vitejs/plugin-vue": "^6.0.0", - "tce-manifest": "workspace:*", - "typescript": "^5.8.3", - "vite": "^7.0.3", - "vue-tsc": "^3.0.1" - }, - "dependencies": { - "stringify-object": "^5.0.0" + "@vitejs/plugin-vue": "^6.0.6", + "tce-counter-manifest": "workspace:*", + "typescript": "^6.0.3", + "vite": "^8.0.8", + "vite-plugin-lib-inject-css": "^2.2.2", + "vue-tsc": "^3.2.6", + "vuetify": "^4.0.5" } } diff --git a/example/packages/display/src/assets/.gitkeep b/examples/counter/packages/display/src/assets/.gitkeep similarity index 100% rename from example/packages/display/src/assets/.gitkeep rename to examples/counter/packages/display/src/assets/.gitkeep diff --git a/examples/counter/packages/display/src/components/Display.vue b/examples/counter/packages/display/src/components/Display.vue new file mode 100644 index 00000000..b3cc26df --- /dev/null +++ b/examples/counter/packages/display/src/components/Display.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/example/packages/display/src/index.ts b/examples/counter/packages/display/src/index.ts similarity index 60% rename from example/packages/display/src/index.ts rename to examples/counter/packages/display/src/index.ts index fa888f11..e4ed3235 100644 --- a/example/packages/display/src/index.ts +++ b/examples/counter/packages/display/src/index.ts @@ -1,5 +1,5 @@ -import baseManifest from 'tce-manifest'; -import type { ElementManifest } from 'tce-manifest'; +import baseManifest from 'tce-counter-manifest'; +import type { ElementManifest } from 'tce-counter-manifest'; import Display from './components/Display.vue'; diff --git a/example/packages/display/src/vite-env.d.ts b/examples/counter/packages/display/src/vite-env.d.ts similarity index 52% rename from example/packages/display/src/vite-env.d.ts rename to examples/counter/packages/display/src/vite-env.d.ts index 11f02fe2..3a945b14 100644 --- a/example/packages/display/src/vite-env.d.ts +++ b/examples/counter/packages/display/src/vite-env.d.ts @@ -1 +1,2 @@ /// +/// diff --git a/example/packages/edit/tsconfig.json b/examples/counter/packages/display/tsconfig.json similarity index 97% rename from example/packages/edit/tsconfig.json rename to examples/counter/packages/display/tsconfig.json index 6af2a0f2..06991e5e 100644 --- a/example/packages/edit/tsconfig.json +++ b/examples/counter/packages/display/tsconfig.json @@ -19,6 +19,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "types": ["vite/client"], + "rootDir": "src", "outDir": "dist" }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], diff --git a/example/packages/display/tsconfig.node.json b/examples/counter/packages/display/tsconfig.node.json similarity index 80% rename from example/packages/display/tsconfig.node.json rename to examples/counter/packages/display/tsconfig.node.json index a645f8f9..548af5ed 100644 --- a/example/packages/display/tsconfig.node.json +++ b/examples/counter/packages/display/tsconfig.node.json @@ -4,7 +4,6 @@ "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, }, "include": ["vite.config.ts"] } diff --git a/example/packages/display/vite.config.ts b/examples/counter/packages/display/vite.config.ts similarity index 72% rename from example/packages/display/vite.config.ts rename to examples/counter/packages/display/vite.config.ts index ab725f86..f36f706b 100644 --- a/example/packages/display/vite.config.ts +++ b/examples/counter/packages/display/vite.config.ts @@ -1,34 +1,30 @@ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; +import { libInjectCss } from 'vite-plugin-lib-inject-css'; import { resolve } from 'node:path'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [vue()], + plugins: [vue(), libInjectCss()], build: { // In order to avoid display runtime issues // due to package missing (if dist is deleted for short time) emptyOutDir: false, minify: false, - cssCodeSplit: false, + lib: { entry: resolve(__dirname, './src/index.ts'), name: 'TceDisplay', fileName: 'index', formats: ['es', 'cjs'], }, - rollupOptions: { + rolldownOptions: { // make sure to externalize deps that shouldn't be bundled // into your library external: ['vue'], output: { - intro: 'import "./index.css";', - // Provide global variables to use in the UMD build - // for externalized deps - globals: { - vue: 'Vue', - }, + exports: 'named', }, }, }, diff --git a/example/packages/edit/eslint.config.js b/examples/counter/packages/edit/eslint.config.js similarity index 100% rename from example/packages/edit/eslint.config.js rename to examples/counter/packages/edit/eslint.config.js diff --git a/example/packages/edit/package.json b/examples/counter/packages/edit/package.json similarity index 66% rename from example/packages/edit/package.json rename to examples/counter/packages/edit/package.json index c03d8198..709db850 100644 --- a/example/packages/edit/package.json +++ b/examples/counter/packages/edit/package.json @@ -1,8 +1,10 @@ { - "name": "tce-edit", - "private": true, - "type": "module", + "name": "tce-counter-edit", + "description": "Counter element edit component", "version": "0.0.1", + "author": "Studion ", + "type": "module", + "private": true, "exports": { ".": { "import": "./dist/index.js", @@ -24,17 +26,19 @@ "prepublish": "pnpm build" }, "peerDependencies": { - "vue": "^3.5.13" + "vue": "^3.5.32" }, "dependencies": { "@tailor-cms/cek-common": "workspace:*" }, "devDependencies": { "@tailor-cms/eslint-config": "workspace:*", - "@vitejs/plugin-vue": "^6.0.0", - "tce-manifest": "workspace:*", - "typescript": "^5.8.3", - "vite": "^7.0.3", - "vue-tsc": "^3.0.1" + "@vitejs/plugin-vue": "^6.0.6", + "tce-counter-manifest": "workspace:*", + "typescript": "^6.0.3", + "vite": "^8.0.8", + "vite-plugin-lib-inject-css": "^2.2.2", + "vue-tsc": "^3.2.6", + "vuetify": "^4.0.5" } } diff --git a/example/packages/edit/src/assets/.gitkeep b/examples/counter/packages/edit/src/assets/.gitkeep similarity index 100% rename from example/packages/edit/src/assets/.gitkeep rename to examples/counter/packages/edit/src/assets/.gitkeep diff --git a/example/packages/edit/src/components/Edit.vue b/examples/counter/packages/edit/src/components/Edit.vue similarity index 60% rename from example/packages/edit/src/components/Edit.vue rename to examples/counter/packages/edit/src/components/Edit.vue index f0551dd6..e449811f 100644 --- a/example/packages/edit/src/components/Edit.vue +++ b/examples/counter/packages/edit/src/components/Edit.vue @@ -23,7 +23,6 @@ accept="image/png, image/jpeg" label="Set background" hide-details - prepend-icon @change="uploadImage" /> -
+
Background image
- - Link example +
+ + + Link element + +
+ + Export data
diff --git a/packages/runtime/preview/src/components/PreviewPanel.vue b/packages/runtime/preview/src/components/PreviewPanel.vue index 5e1a3e92..14dc7231 100644 --- a/packages/runtime/preview/src/components/PreviewPanel.vue +++ b/packages/runtime/preview/src/components/PreviewPanel.vue @@ -45,6 +45,6 @@ onMounted(() => initPanels()); diff --git a/packages/runtime/preview/src/components/SplashLoader.vue b/packages/runtime/preview/src/components/SplashLoader.vue index 86a6ad13..c1992fc5 100644 --- a/packages/runtime/preview/src/components/SplashLoader.vue +++ b/packages/runtime/preview/src/components/SplashLoader.vue @@ -5,7 +5,7 @@
Logo
-
+
Booting Content Element Kit....
v{{ version }}
@@ -44,9 +44,9 @@ onMounted(() => { $splash-color: var(--splash-color); .splash-loader { - flex: 1; - width: 100%; - height: 100%; + position: absolute; + inset: 0; + z-index: 1; display: flex; align-items: center; justify-content: center; @@ -81,7 +81,7 @@ $splash-color: var(--splash-color); transition: opacity 0.3s; } -.fade-enter, +.fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/packages/runtime/preview/src/index.html b/packages/runtime/preview/src/index.html index 2adc871e..d3bc8873 100644 --- a/packages/runtime/preview/src/index.html +++ b/packages/runtime/preview/src/index.html @@ -4,6 +4,7 @@ + Content Element Kit diff --git a/packages/runtime/preview/src/main.ts b/packages/runtime/preview/src/main.ts index 5981b127..abf43728 100644 --- a/packages/runtime/preview/src/main.ts +++ b/packages/runtime/preview/src/main.ts @@ -7,5 +7,10 @@ import { createVuetify } from 'vuetify'; import App from './App.vue'; createApp(App) - .use(createVuetify({ icons: { defaultSet: 'mdi' } })) + .use( + createVuetify({ + theme: { defaultTheme: 'light' }, + icons: { defaultSet: 'mdi' }, + }), + ) .mount('#app'); diff --git a/packages/runtime/preview/tsconfig.json b/packages/runtime/preview/tsconfig.json index d4aefa2c..70c8d506 100644 --- a/packages/runtime/preview/tsconfig.json +++ b/packages/runtime/preview/tsconfig.json @@ -3,13 +3,12 @@ "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, - "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true }, diff --git a/packages/runtime/preview/tsconfig.node.json b/packages/runtime/preview/tsconfig.node.json index 9d31e2ae..5fd89d62 100644 --- a/packages/runtime/preview/tsconfig.node.json +++ b/packages/runtime/preview/tsconfig.node.json @@ -2,8 +2,8 @@ "compilerOptions": { "composite": true, "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true + "moduleResolution": "bundler", + "skipLibCheck": true }, "include": ["vite.config.ts"] } diff --git a/packages/runtime/preview/vite.config.ts b/packages/runtime/preview/vite.config.ts index d61628de..1824fabd 100644 --- a/packages/runtime/preview/vite.config.ts +++ b/packages/runtime/preview/vite.config.ts @@ -18,6 +18,7 @@ Object.entries(config).forEach(([k, v]) => (process.env[`VITE_${k}`] = v)); export default defineConfig((): any => { return { root: './src', + publicDir: '../public', server: { // Accept connections from any host (Docker) host: '0.0.0.0', diff --git a/packages/runtime/tce-display/CHANGELOG.md b/packages/runtime/tce-display/CHANGELOG.md index 7d818539..63df439d 100644 --- a/packages/runtime/tce-display/CHANGELOG.md +++ b/packages/runtime/tce-display/CHANGELOG.md @@ -1,5 +1,27 @@ # @tailor-cms/tce-display-runtime +## 2.0.0-beta.4 + +### Patch Changes + +- Pass initConfig to initState. +- Updated dependencies + - @tailor-cms/cek-common@2.0.0-beta.4 + +## 2.0.0 + +### Major Changes + +- Vuetify 4 migration (MD3 typography, updated component APIs, theme configuration). +- Question auto-wrap: QuestionForm applied automatically by the runtime. +- QuestionContainer renamed to QuestionForm. +- ThemeDialog for testing element appearance with custom Vuetify themes. + +### Patch Changes + +- Updated dependencies + - @tailor-cms/cek-common@2.0.0 + ## 1.3.2 ### Patch Changes diff --git a/packages/runtime/tce-display/package.json b/packages/runtime/tce-display/package.json index ed0efb6a..659e5de1 100644 --- a/packages/runtime/tce-display/package.json +++ b/packages/runtime/tce-display/package.json @@ -2,7 +2,7 @@ "name": "@tailor-cms/tce-display-runtime", "description": "Display content element client side runtime", "author": "Studion ", - "version": "1.3.2", + "version": "2.0.0-beta.10", "type": "module", "scripts": { "dev": "vite", @@ -11,26 +11,23 @@ "nuke": "pnpm dlx del-cli node_modules" }, "dependencies": { - "@mdi/font": "7.4.47", + "@mdi/font": "^7.4.47", "@tailor-cms/cek-common": "workspace:^", - "@vitejs/plugin-vue": "6.0.0", - "@vue/reactivity": "^3.5.17", - "@vue/runtime-core": "^3.5.17", - "@vue/runtime-dom": "^3.5.17", - "@vue/shared": "^3.5.17", - "dotenv": "^17.1.0", - "lodash-es": "^4.17.21", - "sass": "1.89.2", - "typescript": "5.8.3", - "vite": "7.0.3", - "vite-plugin-vuetify": "2.1.1", - "vue": "3.5.17", - "vuetify": "3.9.0" + "@vitejs/plugin-vue": "^6.0.6", + "@vueuse/core": "^14.2.1", + "dotenv": "^17.4.2", + "json5": "^2.2.3", + "lodash-es": "^4.18.1", + "sass": "^1.99.0", + "typescript": "^6.0.3", + "vite": "^8.0.8", + "vue": "^3.5.32", + "vuetify": "^4.0.5" }, "devDependencies": { "@tailor-cms/eslint-config": "workspace:^", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.0.10" + "@types/node": "^24.12.2" }, "publishConfig": { "access": "public" diff --git a/packages/runtime/tce-display/public/favicon.ico b/packages/runtime/tce-display/public/favicon.ico new file mode 100644 index 00000000..0c4c31f7 Binary files /dev/null and b/packages/runtime/tce-display/public/favicon.ico differ diff --git a/packages/runtime/tce-display/public/robots.txt b/packages/runtime/tce-display/public/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/packages/runtime/tce-display/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/packages/runtime/tce-display/src/App.vue b/packages/runtime/tce-display/src/App.vue index d60921d9..db07a278 100644 --- a/packages/runtime/tce-display/src/App.vue +++ b/packages/runtime/tce-display/src/App.vue @@ -1,13 +1,13 @@ diff --git a/packages/runtime/tce-display/src/components/ElementPlaceholder.vue b/packages/runtime/tce-display/src/components/ElementPlaceholder.vue new file mode 100644 index 00000000..02fc728a --- /dev/null +++ b/packages/runtime/tce-display/src/components/ElementPlaceholder.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/runtime/tce-display/src/components/QuestionForm/QuestionFeedback.vue b/packages/runtime/tce-display/src/components/QuestionForm/QuestionFeedback.vue new file mode 100644 index 00000000..fff8ed51 --- /dev/null +++ b/packages/runtime/tce-display/src/components/QuestionForm/QuestionFeedback.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/runtime/tce-display/src/components/QuestionForm/QuestionHint.vue b/packages/runtime/tce-display/src/components/QuestionForm/QuestionHint.vue new file mode 100644 index 00000000..85eea50a --- /dev/null +++ b/packages/runtime/tce-display/src/components/QuestionForm/QuestionHint.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/runtime/tce-display/src/components/QuestionForm/QuestionPrompt.vue b/packages/runtime/tce-display/src/components/QuestionForm/QuestionPrompt.vue new file mode 100644 index 00000000..ae02dd0e --- /dev/null +++ b/packages/runtime/tce-display/src/components/QuestionForm/QuestionPrompt.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/runtime/tce-display/src/components/QuestionForm/index.vue b/packages/runtime/tce-display/src/components/QuestionForm/index.vue new file mode 100644 index 00000000..a6775cdd --- /dev/null +++ b/packages/runtime/tce-display/src/components/QuestionForm/index.vue @@ -0,0 +1,89 @@ + + + diff --git a/packages/runtime/tce-display/src/components/ThemeDialog.vue b/packages/runtime/tce-display/src/components/ThemeDialog.vue new file mode 100644 index 00000000..c2c8fea7 --- /dev/null +++ b/packages/runtime/tce-display/src/components/ThemeDialog.vue @@ -0,0 +1,91 @@ + + + diff --git a/packages/runtime/tce-display/src/components/ThemeForm.vue b/packages/runtime/tce-display/src/components/ThemeForm.vue new file mode 100644 index 00000000..f25bc6c6 --- /dev/null +++ b/packages/runtime/tce-display/src/components/ThemeForm.vue @@ -0,0 +1,101 @@ + + + diff --git a/packages/runtime/tce-display/src/composables/useThemeState.ts b/packages/runtime/tce-display/src/composables/useThemeState.ts new file mode 100644 index 00000000..cf0e7f0b --- /dev/null +++ b/packages/runtime/tce-display/src/composables/useThemeState.ts @@ -0,0 +1,88 @@ +import { computed, watch } from 'vue'; +import { find, kebabCase, reject } from 'lodash-es'; +import JSON5 from 'json5'; +import { useLocalStorage } from '@vueuse/core'; +import { useTheme } from 'vuetify'; + +interface CustomTheme { + name: string; + key: string; + definition: { + dark?: boolean; + colors?: Record; + variables?: Record; + }; +} + +export function useThemeState() { + const { change, computedThemes, themes } = useTheme(); + const activeTheme = useLocalStorage('cek-display-theme-active', 'default'); + const customThemes = useLocalStorage( + 'cek-display-custom-themes', + [], + ); + + customThemes.value.forEach((t) => registerTheme(t)); + + const setTheme = (key: string) => (activeTheme.value = key); + + function registerTheme({ key, definition }: CustomTheme) { + themes.value[key] = { + dark: definition.dark ?? false, + colors: { ...definition.colors }, + variables: { ...definition.variables }, + }; + } + + const removeCustomTheme = (name: string) => { + const key = kebabCase(name); + customThemes.value = reject(customThemes.value, { key }); + if (activeTheme.value === key) setTheme('default'); + }; + + const addCustomTheme = (name: string, input: string) => { + removeCustomTheme(name); + const key = kebabCase(name); + const theme: CustomTheme = { + name, + key, + definition: JSON5.parse(input), + }; + customThemes.value.push(theme); + registerTheme(theme); + setTheme(key); + }; + + const getCustomTheme = (name: string) => + find(customThemes.value, { key: kebabCase(name) }); + + const themeItems = computed(() => [ + { value: 'default', title: 'Light', removable: false }, + { value: 'default-dark', title: 'Dark', removable: false }, + ...customThemes.value.map((t) => ({ + value: t.key, + title: t.name, + removable: true, + })), + ]); + + const getThemeColors = (key: string) => { + const theme = computedThemes.value[key]; + if (!theme?.colors) return []; + return Object.keys(theme.colors).filter( + (color) => !color.startsWith('on-'), + ); + }; + + watch(activeTheme, (key) => change(key), { immediate: true }); + + return { + activeTheme, + addCustomTheme, + getCustomTheme, + getThemeColors, + removeCustomTheme, + setTheme, + themeItems, + }; +} diff --git a/packages/runtime/tce-display/src/index.html b/packages/runtime/tce-display/src/index.html index 6758c17d..12fe3629 100644 --- a/packages/runtime/tce-display/src/index.html +++ b/packages/runtime/tce-display/src/index.html @@ -2,7 +2,9 @@ + + Dev Kit - Content element display preview diff --git a/packages/runtime/tce-display/src/main.ts b/packages/runtime/tce-display/src/main.ts index 7b58e612..2540178c 100644 --- a/packages/runtime/tce-display/src/main.ts +++ b/packages/runtime/tce-display/src/main.ts @@ -1,3 +1,5 @@ +import 'vuetify/styles'; + import { createApp } from 'vue'; import App from './App.vue'; @@ -6,9 +8,22 @@ import NotCompositeAlert from './components/NotCompositeAlert.vue'; import vuetify from './plugins/vuetify'; const element = await import(/* @vite-ignore */ import.meta.env.DISPLAY_DIR); -const isComposite = !!element.default.isComposite; +const { + isComposite = false, + isEmpty = () => false, + isQuestion = false, + name = 'Content Element', + showFeedback = true, + ui = {}, +} = element.default; -const app = createApp(App); +const app = createApp(App, { + icon: ui.icon, + isEmpty, + isQuestion, + name, + showFeedback, +}); app.use(vuetify); app.component( 'TailorEmbeddedContainer', diff --git a/packages/runtime/tce-display/src/plugins/vuetify.ts b/packages/runtime/tce-display/src/plugins/vuetify.ts index 495ecd47..d1ca0b49 100644 --- a/packages/runtime/tce-display/src/plugins/vuetify.ts +++ b/packages/runtime/tce-display/src/plugins/vuetify.ts @@ -1,5 +1,4 @@ import '@mdi/font/css/materialdesignicons.css'; -import 'vuetify/styles'; import * as components from 'vuetify/components'; import * as directives from 'vuetify/directives'; @@ -9,6 +8,13 @@ import { createVuetify } from 'vuetify'; export default createVuetify({ components, directives, + theme: { + defaultTheme: 'default', + themes: { + default: {}, + 'default-dark': { dark: true }, + }, + }, icons: { defaultSet: 'mdi', aliases, diff --git a/packages/runtime/tce-display/src/vite-env.d.ts b/packages/runtime/tce-display/src/vite-env.d.ts index 0654d5f0..de2829b7 100644 --- a/packages/runtime/tce-display/src/vite-env.d.ts +++ b/packages/runtime/tce-display/src/vite-env.d.ts @@ -7,5 +7,5 @@ declare module '*.vue' { } declare module 'vuetify'; -declare module 'vuetify/lib/components'; -declare module 'vuetify/lib/directives'; +declare module 'vuetify/components'; +declare module 'vuetify/directives'; diff --git a/packages/runtime/tce-display/tsconfig.json b/packages/runtime/tce-display/tsconfig.json index 81b33e11..110bdff0 100644 --- a/packages/runtime/tce-display/tsconfig.json +++ b/packages/runtime/tce-display/tsconfig.json @@ -4,10 +4,9 @@ "target": "ESNext", "module": "ESNext", "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "esModuleInterop": true, "skipLibCheck": true, "useDefineForClassFields": true, "strict": false, diff --git a/packages/runtime/tce-display/tsconfig.node.json b/packages/runtime/tce-display/tsconfig.node.json index 06680a39..5fd89d62 100644 --- a/packages/runtime/tce-display/tsconfig.node.json +++ b/packages/runtime/tce-display/tsconfig.node.json @@ -1,9 +1,9 @@ { "compilerOptions": { "composite": true, - "module": "nodenext", - "moduleResolution": "nodenext", - "allowSyntheticDefaultImports": true + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true }, "include": ["vite.config.ts"] } diff --git a/packages/runtime/tce-display/vite.config.ts b/packages/runtime/tce-display/vite.config.ts index 17e495b4..7f0357a5 100644 --- a/packages/runtime/tce-display/vite.config.ts +++ b/packages/runtime/tce-display/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig, UserConfig } from 'vite'; import dotenv from 'dotenv'; import { pick } from 'lodash-es'; import vue from '@vitejs/plugin-vue'; -import vuetify from 'vite-plugin-vuetify'; import { fileURLToPath } from 'url'; import path from 'node:path'; @@ -30,21 +29,19 @@ export default defineConfig((): UserConfig => { const displayModulePath = path.relative(viteConfigPath, TCE_DISPLAY_DIR); return { root: './src', + publicDir: '../public', logLevel: 'warn', server: { // Accept connections from any host (Docker) host: '0.0.0.0', port: parseInt(DISPLAY_RUNTIME_PORT, 10), }, - resolve: { - preserveSymlinks: true, - }, define: { 'import.meta.env.DISPLAY_DIR': JSON.stringify(env.TCE_DISPLAY_DIR), }, optimizeDeps: { include: [displayModulePath.replace(/\/dist$/, '')], }, - plugins: [vue(), vuetify({ autoImport: true })], + plugins: [vue()], }; }); diff --git a/packages/runtime/tce-edit/CHANGELOG.md b/packages/runtime/tce-edit/CHANGELOG.md index 6bf63b6c..c6a7a000 100644 --- a/packages/runtime/tce-edit/CHANGELOG.md +++ b/packages/runtime/tce-edit/CHANGELOG.md @@ -1,5 +1,32 @@ # @tailor-cms/tce-edit-runtime +## 2.0.0-beta.4 + +### Patch Changes + +- Pass initConfig to initState. +- Updated dependencies + - @tailor-cms/cek-common@2.0.0-beta.4 + +## 2.0.0 + +### Major Changes + +- Vuetify 4 migration (MD3 typography, updated component APIs, theme configuration). +- Question auto-wrap: QuestionCard applied automatically by the runtime. +- Element linking: `@link` event, `references` prop, mock link dialog with `mocks.referencesData`. +- Question autosave support (toggle in Settings panel). +- `$rpc` injection for calling server-side procedures. +- `$storageService.upload` accepts a single `File` instead of `FormData`. +- `TailorFileInput` global component (replaces `AssetInput`). +- `TailorElementPlaceholder` global component. +- ThemeDialog for testing element appearance with custom Vuetify themes. + +### Patch Changes + +- Updated dependencies + - @tailor-cms/cek-common@2.0.0 + ## 1.3.2 ### Patch Changes diff --git a/packages/runtime/tce-edit/package.json b/packages/runtime/tce-edit/package.json index 212d3659..b0e1b347 100644 --- a/packages/runtime/tce-edit/package.json +++ b/packages/runtime/tce-edit/package.json @@ -2,7 +2,7 @@ "name": "@tailor-cms/tce-edit-runtime", "description": "Edit content element client side runtime", "author": "Studion ", - "version": "1.3.2", + "version": "2.0.0-beta.10", "type": "module", "scripts": { "dev": "vite", @@ -11,28 +11,25 @@ "nuke": "pnpm dlx del-cli node_modules" }, "dependencies": { - "@mdi/font": "7.4.47", + "@lukeed/uuid": "^2.0.1", + "@mdi/font": "^7.4.47", "@tailor-cms/cek-common": "workspace:^", - "@vitejs/plugin-vue": "6.0.0", - "@vue/reactivity": "^3.5.17", - "@vue/runtime-core": "^3.5.17", - "@vue/runtime-dom": "^3.5.17", - "@vue/shared": "^3.5.17", - "axios": "^1.10.0", - "lodash-es": "^4.17.21", + "@vitejs/plugin-vue": "^6.0.6", + "@vueuse/core": "^14.2.1", + "json5": "^2.2.3", + "ky": "^2.0.1", + "lodash-es": "^4.18.1", "mitt": "^3.0.1", - "sass": "1.89.2", - "typescript": "5.8.3", - "uuid": "^11.1.0", - "vite": "7.0.3", - "vite-plugin-vuetify": "2.1.1", - "vue": "3.5.17", - "vuetify": "3.9.0" + "sass": "^1.99.0", + "typescript": "^6.0.3", + "vite": "^8.0.8", + "vue": "^3.5.32", + "vuetify": "^4.0.5" }, "devDependencies": { "@tailor-cms/eslint-config": "workspace:^", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.0.10" + "@types/node": "^24.12.2" }, "publishConfig": { "access": "public" diff --git a/packages/runtime/tce-edit/public/favicon.ico b/packages/runtime/tce-edit/public/favicon.ico new file mode 100644 index 00000000..0c4c31f7 Binary files /dev/null and b/packages/runtime/tce-edit/public/favicon.ico differ diff --git a/packages/runtime/tce-edit/public/robots.txt b/packages/runtime/tce-edit/public/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/packages/runtime/tce-edit/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/packages/runtime/tce-edit/src/App.vue b/packages/runtime/tce-edit/src/App.vue index da3ac987..39ea6b57 100644 --- a/packages/runtime/tce-edit/src/App.vue +++ b/packages/runtime/tce-edit/src/App.vue @@ -1,111 +1,49 @@ + + diff --git a/packages/runtime/tce-edit/src/components/ElementSettings.vue b/packages/runtime/tce-edit/src/components/ElementSettings.vue new file mode 100644 index 00000000..ca3365b2 --- /dev/null +++ b/packages/runtime/tce-edit/src/components/ElementSettings.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/packages/runtime/tce-edit/src/components/EmbeddedContainer.vue b/packages/runtime/tce-edit/src/components/EmbeddedContainer.vue index 8371e363..31c84cfe 100644 --- a/packages/runtime/tce-edit/src/components/EmbeddedContainer.vue +++ b/packages/runtime/tce-edit/src/components/EmbeddedContainer.vue @@ -45,17 +45,16 @@ + + diff --git a/packages/runtime/tce-edit/src/components/QuestionForm/QuestionFeedback.vue b/packages/runtime/tce-edit/src/components/QuestionForm/QuestionFeedback.vue new file mode 100644 index 00000000..90f771e0 --- /dev/null +++ b/packages/runtime/tce-edit/src/components/QuestionForm/QuestionFeedback.vue @@ -0,0 +1,80 @@ + + + diff --git a/packages/runtime/tce-edit/src/components/QuestionForm/QuestionHint.vue b/packages/runtime/tce-edit/src/components/QuestionForm/QuestionHint.vue new file mode 100644 index 00000000..5b46a8d7 --- /dev/null +++ b/packages/runtime/tce-edit/src/components/QuestionForm/QuestionHint.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/runtime/tce-edit/src/components/QuestionForm/QuestionPrompt.vue b/packages/runtime/tce-edit/src/components/QuestionForm/QuestionPrompt.vue new file mode 100644 index 00000000..50676e9e --- /dev/null +++ b/packages/runtime/tce-edit/src/components/QuestionForm/QuestionPrompt.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/packages/runtime/tce-edit/src/components/QuestionCard.vue b/packages/runtime/tce-edit/src/components/QuestionForm/index.vue similarity index 57% rename from packages/runtime/tce-edit/src/components/QuestionCard.vue rename to packages/runtime/tce-edit/src/components/QuestionForm/index.vue index b2450a0b..1d8d3fef 100644 --- a/packages/runtime/tce-edit/src/components/QuestionCard.vue +++ b/packages/runtime/tce-edit/src/components/QuestionForm/index.vue @@ -1,19 +1,43 @@