<!--
	covers: resources webpack.config.js package.json
	verified: 54fd0a6
-->
# Frontend

Webpack Encore, one entry per page, Bootstrap 5, jQuery-based widget classes.

## Layout of `resources/`

```
resources/js/
  entries/       one file per page — <controllerId><ActionId>.js
  widgets/       widget classes, one per PHP widget, extending Widget.js
  components/    reusable behaviours that are not widgets
  services/      SvgLoader.js
  admin/         admin-only JS
  jquery-ui-1.13.1.custom/
resources/scss/
  entries/       one per JS entry
  widgets/       one per widget, mirroring its CSS class
  components/    button, checkbox, dialog, form, photo-zoom, selectize, simple-lightbox, table
  admin/
  variables.scss  bootstrap-core.scss  bootstrap.scss  fontawesome.scss  maps.scss  animations.scss
```

## Entries

[`webpack.config.js`](../../webpack.config.js) lists **25 entries**, built into `web/build/`:

- 22 page entries named `<controllerId><ActionId>` — `articlesIndex`, `storagesDisplay`, `usersAccount`, …
- `common` — always loaded, prepended to every page by `View::getEntryList()`
- `admin` — the whole backend, one entry
- `ckeditor`

The name is **derived, not configured**: `View::getEntryList()` builds it as
`$controller->id . Inflector::id2camel( $controller->action->id )`. Adding a page action therefore means
adding an entry with exactly that name, or the page silently gets only `common`.

An entry looks like this ([`resources/js/entries/articlesIndex.js`](../../resources/js/entries/articlesIndex.js)):

```js
// Import styles
import '../../scss/entries/articlesIndex.scss';

// Import widgets
import ArticleWidget from '../widgets/ArticleWidget';
import PaginationWidget from '../widgets/PaginationWidget';

// Initialize widgets
PaginationWidget.init();
ArticleWidget.init();
```

SCSS first, then widgets, then `init()` calls. `./yii dev/create-entry <controllerId> <actionId> [widgets…]`
writes this file for you.

### Build configuration worth knowing

| Setting | Effect |
|---------|--------|
| `disableSingleRuntimeChunk()` | every entry is standalone — no shared runtime file to load |
| `enableVersioning( isProduction )` | production filenames get a content hash; dev filenames do not |
| `autoProvidejQuery()` | `$` and `jQuery` are global without importing |
| `configureCssLoader` url filter | URLs starting with `/images/` are **not** resolved by webpack — they stay absolute and are served from `web/images/` |
| `enableSourceMaps( !isProduction )` | no source maps in production |
| Terser + css-minimizer | only in production |

`cleanupOutputBeforeBuild()` wipes `web/build/` on every build — which is why a dev build must never be
committed. See [../COMMIT.md](../COMMIT.md).

## How assets reach the page

`View` reads `web/build/entrypoints.json`:

- **CSS is inlined.** `registerEncoreStyles()` `file_get_contents()`s each stylesheet and echoes it inside a
  `<style>` tag. A `<link>` is only used if reading the file fails.
- **JS is linked**, preceded by an inline `const APP_CONFIG = …` built from
  `Controller::getJsConfig()`.
- Both add an `integrity` attribute when `entrypoints.json` carries one.

A missing `entrypoints.json` degrades to no assets at all, silently. See
[REQUEST-LIFECYCLE.md](REQUEST-LIFECYCLE.md).

## The JS widget base

[`resources/js/widgets/Widget.js`](../../resources/js/widgets/Widget.js) — 30 lines, and every frontend widget
class extends it:

```js
class Widget {
    static selector = '.widget';
    elem = null;
    constructor( elem ) {
        this.elem = elem;
        this.elem.addClass( 'widget-initialized' );
    }
    static init() {
        this.initializeInstances();
        window.addEventListener( 'html:updated', () => this.initializeInstances() );
    }
    static initializeInstances() {
        $( this.selector + ':not(.widget-initialized)' ).each( ( i, elem ) => new this( $( elem ) ) );
    }
}
```

Three things follow from this:

1. **`static selector` must be set** on every subclass, or it inherits `.widget` and initializes everything.
2. **`widget-initialized` is the idempotence guard.** Instances are never constructed twice for the same
   element.
3. **`html:updated` is the AJAX contract.** Any code that injects markup must dispatch
   `window.dispatchEvent( new Event( 'html:updated' ) )`, or the new widgets stay dead. This is how the
   storage search re-arms its widgets after a filter request.

Subclasses call `super( elem )` in the constructor and do their work there — there is no separate lifecycle
hook.

## Components vs widgets

| | Widget | Component |
|---|---|---|
| Where | `resources/js/widgets/` | `resources/js/components/` |
| Has a PHP counterpart | yes, one-to-one | no |
| Bound by | `static selector` + `init()` | constructed explicitly |
| Examples | `StorageFilterWidget`, `CalendarWidget` | `Dialog`, `Selectize`, `Geo`, `LazyImage`, `CookieConsent`, `PhoneFormatter` |

Components are the shared behaviours several widgets need. They are not auto-initialized.

## SCSS conventions

One file per widget in `resources/scss/widgets/`, named after the widget's CSS class, importing the shared
core and scoping everything under that class:

```scss
/**
 * Article widget's style.
 */

@import '../bootstrap-core';

.article-widget {
    background: $white;
    p { line-height: 1.3em; }
}
```

`bootstrap-core` carries the variables and mixins only — it emits no CSS, so importing it in every file is
free. `bootstrap.scss` is the full framework and is imported once, from `common`.

Bootstrap 5 utility classes first; write custom CSS only where utilities do not reach. Shared variables live
in `resources/scss/variables.scss`.

## Frontend libraries

Bootstrap 5.3, jQuery, Swiper, Selectize, easepick, SimpleLightbox, axios, lodash, the Google Maps loader,
vanilla-cookieconsent, FontAwesome Pro.

## Traps

1. **A new page action without an entry gets only `common`** — no error, just missing behaviour.
2. **`cleanupOutputBeforeBuild()` + versioning** means a dev build and a production build cannot coexist in
   `web/build/`. Whatever ran last is what is there.
3. **Injected markup needs `html:updated`.** Widgets do not observe the DOM.
4. **`/images/` URLs bypass webpack.** Referencing an image from SCSS under any other path makes webpack try
   to resolve it at build time.
5. **CSS is inlined into every response**, so a large stylesheet is paid for on every page load rather than
   cached. Keep entry stylesheets narrow.
