diff --git a/docs/dev/web/development/_category_.json b/docs/dev/web/development/_category_.json index 5bed99fc3..d07b6dbf2 100644 --- a/docs/dev/web/development/_category_.json +++ b/docs/dev/web/development/_category_.json @@ -1,9 +1,4 @@ { "label": "Development", - "position": 2, - "link": { - "type": "generated-index", - "description": "\uD83D\uDC69\u200D\uD83D\uDCBB Development", - "slug": "dev/web/development/" - } + "position": 2 } diff --git a/docs/dev/web/development/index.md b/docs/dev/web/development/index.md new file mode 100644 index 000000000..4a5586d1c --- /dev/null +++ b/docs/dev/web/development/index.md @@ -0,0 +1,9 @@ +--- +title: 'Development' +--- + +This section covers the development workflow and conventions for contributing to OpenCloud Web. + +- [Tooling](./tooling) describes the local development setup and the tools used to build OpenCloud Web. +- [Repository structure and published packages](./repo-structure) explains the most important parts of the Web repository. +- [Conventions](./conventions) summarizes the coding and contribution conventions used by the project. diff --git a/docs/dev/web/extension-system/advanced-topics/_category_.json b/docs/dev/web/extension-system/advanced-topics/_category_.json new file mode 100644 index 000000000..c1aec597f --- /dev/null +++ b/docs/dev/web/extension-system/advanced-topics/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Advanced topics", + "position": 3 +} diff --git a/docs/dev/web/extension-system/advanced-topics/configuration.md b/docs/dev/web/extension-system/advanced-topics/configuration.md new file mode 100644 index 000000000..31436976a --- /dev/null +++ b/docs/dev/web/extension-system/advanced-topics/configuration.md @@ -0,0 +1,57 @@ +--- +title: 'Configuration' +sidebar_position: 1 +--- + +This page describes how to add configuration options to your app. Administrators can then change the behaviour of your +app. + +## Manifest and defaults + +Put static metadata of your app into `src/manifest.json`. It ends up in the `manifest.json` of your build, together with +the `name`, `version`, `description`, `license` and `author` fields of your `package.json`, and the entry point of your +app. + +Default values for the app config belong under the `config` key: + +```json title="src/manifest.json" +{ + "config": { + "maxFileSize": 10485760, + "showPreview": true + } +} +``` + +## Overriding values + +There are two ways to override the defaults: + +- During development, put your values into `src/config.json`. The file is merged on top of the `config` key of + `src/manifest.json`. Don't commit values that only apply to your machine. +- In a real deployment, administrators override them via the `apps.yaml` file, see the + [web applications admin docs](../../../../admin/configuration/web-applications). + +```json title="src/config.json" +{ + "showPreview": false +} +``` + +## Reading values in your app + +The merged config is passed to the `setup` function of your app definition as `applicationConfig`: + +```typescript title="src/index.ts" +import { defineWebApplication } from '@opencloud-eu/web-pkg'; + +export default defineWebApplication({ + setup({ applicationConfig }) { + const showPreview = applicationConfig?.showPreview ?? true; + + // ... + } +}); +``` + +Always provide a fallback value. The config can be empty, and an administrator can set any value. diff --git a/docs/dev/web/extension-system/advanced-topics/index.md b/docs/dev/web/extension-system/advanced-topics/index.md new file mode 100644 index 000000000..dd2808ae7 --- /dev/null +++ b/docs/dev/web/extension-system/advanced-topics/index.md @@ -0,0 +1,10 @@ +--- +title: 'Advanced topics' +--- + +This section covers additional concepts for developing and maintaining OpenCloud Web apps. + +- [Configuration](./configuration) explains how to define defaults and expose configuration options to administrators. +- [Styling](./styling) describes how to use the OpenCloud design system, color roles, and Tailwind CSS. +- [Translations](./translations) explains how to make an app available in multiple languages. +- [Testing](./testing) introduces the available test setup and helpers. diff --git a/docs/dev/web/extension-system/advanced-topics/styling.md b/docs/dev/web/extension-system/advanced-topics/styling.md new file mode 100644 index 000000000..5d53fe175 --- /dev/null +++ b/docs/dev/web/extension-system/advanced-topics/styling.md @@ -0,0 +1,59 @@ +--- +title: 'Styling' +sidebar_position: 2 +--- + +This page describes how to style your app so that it fits into OpenCloud Web. + +## Design system components + +The `@opencloud-eu/design-system` package contains the components that OpenCloud Web is built with, for example buttons, +inputs and modals. Use them where possible. Your app then follows the platform look and inherits accessibility and +theming for free. See the [design system documentation](../../design-system) for the list of components. + +## Color roles + +OpenCloud Web exposes its colors as CSS variables with the `--oc-role-` prefix, for example `--oc-role-surface` or +`--oc-role-on-surface`. Always use these variables instead of fixed color values. They change with the active theme and +with the color scheme, so your app stays readable in light and dark mode. + +```css +.my-panel { + background-color: var(--oc-role-surface-container); + color: var(--oc-role-on-surface); +} +``` + +See [the defaults](https://github.com/opencloud-eu/web/blob/main/packages/design-system/src/styles/defaults.css) for a +complete list of color roles. + +## Tailwind CSS + +The `@opencloud-eu/extension-sdk` package ships a preconfigured [Tailwind CSS](https://tailwindcss.com/) setup. Import +its stylesheet in your entrypoint (typically `src/index.ts`) to use it: + +```typescript title="src/index.ts" +import '@opencloud-eu/extension-sdk/tailwind.css'; +``` + +Tailwind classes need the `ext:` prefix in apps. The prefix avoids style conflicts with the OpenCloud Web runtime. + +```html +
...
+``` + +The setup maps the color roles to Tailwind color utilities, so `--oc-role-surface` is available as `bg-role-surface`, +`text-role-on-surface` and so on: + +```html +
...
+``` + +It also sets the OpenCloud font family, a spacing unit of `4px`, the breakpoints `xs`, `sm`, `md`, `lg` and `xl`, and a +`dark` variant. The `dark` variant follows the color scheme of OpenCloud Web, not the browser setting: + +```html +
...
+``` + +In most cases you do not need the `dark` variant, because the color roles already handle both color schemes. diff --git a/docs/dev/web/extension-system/advanced-topics/testing.md b/docs/dev/web/extension-system/advanced-topics/testing.md new file mode 100644 index 000000000..5a8094d4c --- /dev/null +++ b/docs/dev/web/extension-system/advanced-topics/testing.md @@ -0,0 +1,41 @@ +--- +title: 'Testing' +sidebar_position: 4 +--- + +This page describes how to test your app. + +## Unit tests + +The [web-app-skeleton repository](https://github.com/opencloud-eu/web-app-skeleton) comes with a +[vitest](https://vitest.dev/) setup. Run the unit tests with: + +```bash +pnpm test:unit +``` + +## Test helpers + +The `@opencloud-eu/web-test-helpers` package provides utilities for mounting components with a mocked OpenCloud Web +context. Without them, every component that uses a composable of `web-pkg` fails to mount. + +```typescript title="tests/unit/App.spec.ts" +import { defaultPlugins, mount } from '@opencloud-eu/web-test-helpers'; +import App from '../../src/App.vue'; + +describe('App', () => { + it('renders the title', () => { + const wrapper = mount(App, { global: { plugins: [...defaultPlugins()] } }); + expect(wrapper.text()).toContain('My app'); + }); +}); +``` + +For details, please refer to the package's +[README.md](https://github.com/opencloud-eu/web/blob/main/packages/web-test-helpers/README.md). + +## End to end tests + +For end to end tests with [Playwright](https://playwright.dev/), the +[web](https://github.com/opencloud-eu/web) and [web-extensions](https://github.com/opencloud-eu/web-extensions) +repositories contain working examples. diff --git a/docs/dev/web/extension-system/advanced-topics/translations.md b/docs/dev/web/extension-system/advanced-topics/translations.md new file mode 100644 index 000000000..0485a2686 --- /dev/null +++ b/docs/dev/web/extension-system/advanced-topics/translations.md @@ -0,0 +1,138 @@ +--- +title: 'Translations' +sidebar_position: 3 +--- + +OpenCloud Web uses [gettext](https://www.gnu.org/software/gettext/) for translations, via the +[vue3-gettext](https://jshmrtn.github.io/vue3-gettext/) library. Your app brings its own translations and hands them to +the Web runtime. The language that the user picked in OpenCloud is then also used for your app. + +Translations are optional. An app without them simply shows its original strings. + +## Marking strings + +In a script or a composable, get `$gettext` from `useGettext`: + +```typescript +import { useGettext } from 'vue3-gettext'; + +const { $gettext } = useGettext(); + +const title = $gettext('Your application name'); +``` + +In a template, `$gettext` is globally available: + +```html + +``` + +Pass variables as an object. Use the `%{name}` syntax in the string, never string concatenation, because the word order +changes between languages: + +```typescript +$gettext('Open «%{resource}»', { resource: resource.name }); +``` + +For plurals, use `$ngettext` with the singular form, the plural form, the count, and the variables: + +```typescript +const { $ngettext } = useGettext(); + +$ngettext( + 'Delete the selected resource?', + 'Delete %{amount} selected resources?', + resources.length, + { amount: resources.length.toString() } +); +``` + +:::warning +The extraction tool reads your source code, not your runtime values. Always pass a plain string literal to `$gettext`. +A variable or a template literal cannot be extracted. +::: + +## Setting up the workflow + +Add [vue3-gettext](https://jshmrtn.github.io/vue3-gettext/) as a dev-dependency. It ships the `vue-gettext-extract` and +`vue-gettext-compile` commands. + +Create a `gettext.config.cjs` in the root of your app. It defines which files are scanned and which languages you +support: + +```javascript title="gettext.config.cjs" +module.exports = { + input: { + path: './src', + include: ['**/*.js', '**/*.ts', '**/*.vue'] + }, + output: { + locales: ['de', 'es', 'fr', 'it'], + path: './l10n/locale', + potPath: '../template.pot', + jsonPath: '../translations.json', + flat: false, + linguas: false + } +}; +``` + +Add two scripts to your `package.json`: + +```json title="package.json" +{ + "scripts": { + "l10n:extract": "vue-gettext-extract", + "l10n:compile": "vue-gettext-compile" + } +} +``` + +`l10n:extract` collects all marked strings into `l10n/template.pot` and creates one `.po` file per language under +`l10n/locale`. Run it whenever you add or change a string. + +`l10n:compile` turns the `.po` files into a single `l10n/translations.json`. Run it after the translated `.po` files +come back. Commit `translations.json`, because the build imports it. + +How the `.po` files get translated is up to you. The OpenCloud repositories use +[Transifex](https://www.transifex.com/), but any gettext based service or a manual workflow works as well. + +## Registering translations + +Import `translations.json` and return it from your app definition. The Web runtime merges it into the global +translations when your app is loaded. + +```typescript title="src/index.ts" +import { defineWebApplication } from '@opencloud-eu/web-pkg'; +import { useGettext } from 'vue3-gettext'; +import translations from '../l10n/translations.json'; + +export default defineWebApplication({ + setup() { + const { $gettext } = useGettext(); + + return { + appInfo: { + name: $gettext('Your application name'), + id: 'your-app' + }, + translations + }; + } +}); +``` + +The file is a map of language key to message map: + +```json title="l10n/translations.json" +{ + "de": { + "No files here": "Keine Dateien hier" + }, + "fr": { + "No files here": "Aucun fichier ici" + } +} +``` diff --git a/docs/dev/web/extension-system/build-and-publish.md b/docs/dev/web/extension-system/build-and-publish.md new file mode 100644 index 000000000..238c63a6c --- /dev/null +++ b/docs/dev/web/extension-system/build-and-publish.md @@ -0,0 +1,104 @@ +--- +title: 'Build and publish' +sidebar_position: 2 +--- + +This page describes how to build your app for production and how to publish it in the OpenCloud app store. + +## Building an app + +Run a production build: + +```bash +pnpm build +``` + +The build writes your app into the `dist` folder. Static assets from a `public` folder are copied over as is. The result +contains: + +- `js/-.mjs` - the entry point of your app, plus its chunks. +- `manifest.json` - the metadata that OpenCloud uses to discover and load your app. +- any static assets of your app. + +The generated `manifest.json` merges your `src/manifest.json` with the `name`, `version`, `description`, `license` and +`author` fields of your `package.json`. The `entrypoint` key is set by the build. + +```json title="dist/manifest.json" +{ + "name": "my-app", + "version": "1.0.0", + "description": "My OpenCloud app", + "license": "Apache-2.0", + "author": "Me", + "entrypoint": "js/my-app-a1b2c3d4.mjs" +} +``` + +## Publishing in the app store + +Apps in the OpenCloud app store are listed in the +[awesome-apps repository](https://github.com/opencloud-eu/awesome-apps). Publishing means adding your app to its +`webApps/apps.json` file via a pull request. + +### Requirements + +- Your app must be downloadable as a `.zip` file from a stable URL, for example a GitHub release asset. +- The zip must contain a `manifest.json` in its root, next to your built files. A production build produces this + already, so zip the content of your `dist` folder, not the folder itself. + +### apps.json entry + +Add one entry under the `apps` key. The schema is defined in the +[app store types](https://github.com/opencloud-eu/web/blob/main/packages/web-app-app-store/src/types.ts). + +```json title="webApps/apps.json" +{ + "id": "com.github.my-org.my-repo.my-app", + "name": "My App", + "subtitle": "One short line about what the app does.", + "description": "A longer description.", + "license": "Apache-2.0", + "versions": [ + { + "version": "1.0.0", + "minOpenCloud": "8.0.0", + "url": "https://github.com/my-org/my-repo/releases/download/v1.0.0/my-app-1.0.0.zip" + } + ], + "authors": [{ "name": "My Organization", "url": "https://example.org" }], + "tags": ["editor", "viewer"], + "coverImage": { + "url": "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/main/webApps/my-org/my-repo/cover.png" + }, + "screenshots": [ + { + "url": "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/main/webApps/my-org/my-repo/screenshots/1.png", + "caption": "What this screenshot shows" + } + ], + "resources": [ + { "url": "https://github.com/my-org/my-repo", "label": "Source code", "icon": "github" } + ] +} +``` + +Notes on the fields: + +- `versions` must be sorted from newest to oldest. `minOpenCloud` is the lowest OpenCloud version your app supports. +- `badge` is optional and accepts a `label` and a `color` of `primary`, `success` or `danger`. +- `screenshots` and `resources` are optional. + +### Assets + +Cover image and screenshots are stored in the awesome-apps repository. Use a 3:2 aspect ratio and prefer PNG or JPEG. + +Follow this folder structure: + +- `webApps///cover.png` +- `webApps///screenshots/1.png` + +If your repository hosts several apps, add the app name to the path, for example +`webApps////cover.png`. + +Make sure the license of your assets allows us to keep them in the repository and to show them in the app store. If you +are not sure about the license of an asset, do not add it. diff --git a/docs/dev/web/extension-system/extension-types/index.md b/docs/dev/web/extension-system/extension-types/index.md deleted file mode 100644 index a6f04677d..000000000 --- a/docs/dev/web/extension-system/extension-types/index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Extension Types' ---- - -This section is a guide about the different predefined extension types of OpenCloud Web. Please refer to the respective -subpages to learn more about the individual extension types. diff --git a/docs/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md b/docs/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md deleted file mode 100644 index 7d7e171e0..000000000 --- a/docs/dev/web/extension-system/extension-types/left-sidebar-menu-item-extensions.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: 'Left sidebar menu item extensions' -sidebar_position: 5 -id: left-sidebar-menu-item-extensions ---- - -## Left sidebar menu item extension type - -One possible extension type is left sidebar menu items. Registered left sidebar menu items get rendered in the left sidebar, as long as there is more than one available. - -### Configuration - -To define a left sidebar menu item, you implement the SidebarNavExtension interface. -It looks like this: - -```typescript -interface SidebarNavExtension { - id: string - type: 'sidebarNav' - extensionPointIds?: string[] - navItem: AppNavigationItem // Please check the AppNavigationItem section below - } -} -``` - -For `id`, `type`, and `extensionPointIds`, please see [extension base section](../#extension-base-configuration) in the top level docs. - -#### AppNavigationItem - -The most important configuration options are: - -- `icon` - The icon to be displayed, can be picked from [Remix Icon](https://remixicon.com/) -- `name` - The text to be displayed -- `route` - The string/route to navigate to, if the nav item should be a `` (Mutually exclusive with `handler`) -- `handler` - The action to perform upon click, if the nav item should be a `