diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16e76a59..1569215d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,9 @@ jobs: - name: Run tests run: yarn test + - name: Run typecheck tests + run: yarn vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json + - name: Run release [${{ inputs.prerelease }}] if: ${{ inputs.prerelease != 'none' }} run: yes | npx tsx scripts/release.ts -p ${{ inputs.prerelease }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2cc84d9..553d9875 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,3 +90,6 @@ jobs: - name: Run tests run: yarn test + + - name: Run typecheck tests + run: yarn vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0f4da1d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md + +## Goals +- Avoid clarification loops by proposing a concrete interpretation when details are missing. +- Default to the language of the user's initial message unless they explicitly request a different language. +- Match the tone and formality of the user's initial message unless they explicitly ask for a change. +- Treat a language switch in the user's message as an explicit request to respond in that language. +- If a message is mixed-language, reply in the dominant language unless the user specifies otherwise. + +## Purpose +This file defines practical instructions for working in the `@retailcrm/embed-ui` repository. + +## Repository Structure +- This project is a Yarn Workspaces monorepo. +- Workspace glob: `packages/*`. +- Current workspace folders: + - `v1-components` + - `v1-contexts` + - `v1-testing` + - `v1-types` +- Workspace package names may differ from folder names, but commit scopes in this repository are based on workspace folder names. + +## Local Environment Prerequisites +- Yarn version is `4.12.0` (see `packageManager` in `package.json` and `yarnPath` in `.yarnrc.yml`). +- Package manager mode is `node-modules` (see `.yarnrc.yml`). +- Local Yarn config is generated from `.yarnrc.dist.yml` using: +```bash +make .yarnrc.yml +``` +- Root install: +```bash +yarn install +``` + +## Running Checks + +### Local Path (without Docker) +- Prepare Yarn config: +```bash +make .yarnrc.yml +``` +- Install dependencies: +```bash +yarn install +``` +- Build all workspaces: +```bash +yarn workspaces foreach -A --topological-dev run build +``` +- Run lint: +```bash +yarn eslint +``` +- Run tests: +```bash +yarn test +``` + +### Docker Path (Makefile) +- Install dependencies in container: +```bash +make node_modules +``` +- Build all workspaces: +```bash +make build +``` +- Run tests: +```bash +make tests +``` +- Pass custom Vitest CLI arguments via Makefile: +```bash +make tests cli="-t outOfRangeErrorText" +``` + +## Related Commands +- Build root package only: +```bash +yarn build +``` +- Build root code artifacts only: +```bash +yarn build:code +``` +- Build root meta artifacts only: +```bash +yarn build:meta +``` +- Build Storybook for `v1-components`: +```bash +yarn workspace @retailcrm/embed-ui-v1-components run storybook:build +``` + +## Commit Workflow +- Commit format: Conventional Commits. +- Commit message language: English. +- Scope rule: use workspace folder name (not npm package name). +- Valid workspace scopes currently are: + - `v1-components` + - `v1-contexts` + - `v1-testing` + - `v1-types` +- For root/global changes, scope may be omitted. +- Split commits by logical intent. +- Keep commit subject concise and factual. +- Start commit subject description with an uppercase letter. +- Mention affected component(s) or area in subject description when applicable. +- Commit subject should describe completed change in past tense. +- Prefer passive voice for changelog-friendly phrasing. +- Do not amend/rewrite history unless explicitly requested. + +Examples: +- `feat(v1-components): UiSelect searchable option group header added` +- `fix(v1-contexts): OrderContext missing customer id handling corrected` +- `docs: AGENTS commit workflow section updated` + +## Skills +- Repository-local skills are available under `skills/`. +- If a skill conflicts with this file, follow `AGENTS.md`. +- Current local skills: + - `skills/commit-workflow/SKILL.md` + - `skills/sync-remote-host-registry/SKILL.md` + - `skills/yarn-lock-conflict-resolution/SKILL.md` + +## Notes +- Do not assume legacy rules from other repositories (especially `omnica`) apply here. +- If repository policy is unclear, ask a short clarifying question before making irreversible actions. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..99614531 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,143 @@ +# Архитектура `@retailcrm/embed-ui` + +## 1. Назначение + +Репозиторий содержит библиотечный слой для JS-расширений RetailCRM: + +- контракт и транспорт между `remote` (код расширения) и `host` (CRM-страница); +- UI-компоненты с разделением на remote-описания и host-реализации; +- реактивные контексты (предопределенные и пользовательские); +- вспомогательные типы и тестовые утилиты. + +Ключевая идея: расширение не рендерит «реальный DOM» CRM напрямую. +Оно отправляет инструкции рендера и вызовов в host-часть через RPC/remote-runtime. + +## 2. Структура монорепозитория + +Монорепозиторий на Yarn Workspaces (`packages/*`), Yarn `4.12.0`, linker `node-modules`. + +### `src/` (корневой пакет `@retailcrm/embed-ui`) + +- точка входа SDK для расширений; +- `createWidgetEndpoint(widget, messenger)` поднимает endpoint и lifecycle `run/release`; +- создает remote-root с whitelist host-компонентов (`createRoot`); +- подключает Pinia + плагины доступа к контекстам (`injectEndpoint`, `injectAccessor`); +- экспортирует публичный API контекстов и composables (`useField`, `useCustomField`, `useHost`, `useRouter`). + +### `packages/v1-components` + +- UI-библиотека разделена на два entrypoint: +- `src/host.ts`: host-компоненты для реального рендера в CRM; +- `src/remote.ts`: remote-компоненты (описания/схемы для передачи в host). +- Storybook живет здесь и используется как витрина компонентов и сценариев. + +### `packages/v1-contexts` + +- слой реактивных контекстов и action-вызовов; +- `src/host.ts`: сборка host-accessor’ов (`createGetter`, `createSetter`, `createContextAccessor`, `createCustomContextAccessor`) и типизированные ошибки (`HostError`, `LogicalError`, `RuntimeError`); +- `src/remote.ts`: Pinia-обертки для предопределенных контекстов (`defineContext`) и инвокаций (`defineActions`); +- `src/remote/custom.ts`: доступ к пользовательским полям и словарям. + +### `packages/v1-types` + +- базовые TS-контракты (контексты, поля, отклонения, action-схемы и пр.); +- используется всеми остальными workspace. + +### `packages/v1-testing` + +- тестовые утилиты для runtime-сценариев (RPC/polyfill и хелперы событий). + +## 3. Runtime-модель host/remote + +### 3.1 Поток инициализации виджета + +1. CRM (host) создает `messenger` и вызывает `createWidgetEndpoint(...)` из корневого SDK. +2. SDK поднимает RPC endpoint и экспортирует методы lifecycle: `run(channel, target)`, `release()`. +3. При `run(...)` создается remote-root с допустимыми host-компонентами, инициализируется Pinia с endpoint-плагинами контекстов, после чего вызывается `widget.run(createApp, root, pinia, target)`. +4. При `release()` вызывается destroy-функция виджета и освобождаются RPC-ресурсы. + +### 3.2 Каналы ответственности + +- `remote`: бизнес-логика расширения, декларативная сборка UI через remote-компоненты, чтение/запись контекстов через Pinia stores. +- `host`: фактический рендер, геометрия, DOM, popper/positioning, интерактивность, предоставление данных контекста и invokable-методов (`goTo`, `httpCall`, domain actions). + +## 4. Архитектура компонентов + +### 4.1 Общий паттерн + +Для большинства компонентов сохраняется 1:1 соответствие: + +- remote-компонент описывает схему и события; +- host-компонент выполняет фактический рендер. + +Примеры: `UiButton`, `UiCheckbox`, `UiTooltip`. + +### 4.2 Важный частный случай: `UiSelect` + +`UiSelect` — первый явно композиционный пример, где соответствие не 1:1: + +- remote-слой содержит семейство: `UiSelect`, `UiSelectOption`, `UiSelectOptionGroup`, `UiSelectOptionGroupHeader`; +- host-рендер опирается на `UiSelectTrigger` + `UiSelectPopper` + menu/popper-примитивы; +- состояние выбора/фильтрации и регистрация опций координируются через `provide/inject`-ключи. + +Следствие: host-реестр компонентов должен отражать не только «публичные remote-компоненты», но и необходимые host-примитивы, из которых они фактически собираются. + +### 4.3 Реестр host-компонентов: что в него входит + +Реестр в `src/index.ts` должен содержать только те host-компоненты, которые достижимы из remote-схемы рендера. + +- часть host-компонентов может быть внутренней (не remote-публичной) и потому не обязана попадать в реестр root SDK; +- обратный случай тоже возможен: один remote-компонент может требовать несколько host-примитивов; +- пример host-only компонента: `UiToolbar` экспортируется в `v1-components/host`, но не участвует в текущем remote render graph корневого SDK. + +## 5. Контексты и данные + +### 5.1 Предопределенные контексты + +- контекст задается описанием схемы полей; +- remote-store инициализируется через `endpoint.call.get(context, '~')`; +- подписки на изменения поля устанавливаются через `endpoint.call.on('change:field', ...)`; +- запись идет через `endpoint.call.set(...)` с валидацией по schema. + +### 5.2 Пользовательские контексты (custom fields) + +- схема контекста запрашивается динамически у host; +- значения и типы полей становятся известны после `initialize()`; +- чтение/запись выполняются через `getCustomField/setCustomField`; +- словари грузятся отдельным каналом `getCustomDictionary`. + +### 5.3 Ошибки и отклонения + +- host-часть разделяет логические и runtime-ошибки; +- remote-часть может получать `Rejection` через callback `onReject`; +- в composables заданы безопасные обработчики по умолчанию (логирование в `console.error`). + +## 6. Storybook как архитектурная песочница + +`packages/v1-components/storybook` выполняет две роли: + +- документация и интерактивная проверка host-компонентов; +- демонстрация host/remote-связки (сейчас явно показана в `UiSelect`-истории через worker + endpoint + HostedTree/provider/receiver). + +Текущее состояние: bridge-механика визуально раскрыта не во всех историях, что усложняет onboarding для новых участников. + +## 7. Сборка, релиз, CI + +- сборка корня: `vite build` + генерация meta (`scripts/build.meta.ts`); +- workspace-сборка в CI: `yarn workspaces foreach -A --topological-dev run build`; +- тестовый workflow: Node `22.x` и `24.x`, шаги build + eslint + test; +- release workflow: build/lint/test, bump/release scripts, публикация npm, деплой Storybook (`version` + обновление `latest`). + +## 8. Дальнейшее развитие качества + +Следующий этап развития проекта направлен на усиление автоматизированного контроля качества и формализацию ключевых архитектурных инвариантов. + +## 9. Планы по усилению контроля качества + +Ближайшие шаги: + +1. Ввести обязательный typecheck в локальный и CI-пайплайн. +2. Расширить тесты на ключевые контракты host/remote и на синхронизацию реестров компонентов. +3. Добавить автоматические проверки целостности render graph (минимум smoke/contract test). +4. Расширить Storybook-документацию: явно показывать host/remote-binding не только для `UiSelect`. +5. После стабилизации перейти к автоматизации registry/provider (кодогенерация) как к следующему этапу эволюции. diff --git a/Makefile b/Makefile index 8c094476..2fff1bb2 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,21 @@ else $(YARN) test endif +.PHONY: tests-typecheck-contexts +tests-typecheck-contexts: ## Runs typecheck tests (test-d.ts) for v1-contexts + $(TARGET_HEADER) +ifdef cli + $(YARN) vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json $(cli) +else + $(YARN) vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json +endif + +.PHONY: tests-typecheck-v1-contexts +tests-typecheck-v1-contexts: tests-typecheck-contexts ## Alias for tests-typecheck-contexts + +.PHONY: tests-typecheck +tests-typecheck: tests-typecheck-contexts ## Runs typecheck tests (currently v1-contexts) + .PHONY: help help: ## Calls recipes list @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk '\ diff --git a/eslint.config.js b/eslint.config.js index 73e4e7f3..66b3830f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,6 +15,9 @@ export default [ }, }, rules: { + 'brace-style': ['error', '1tbs', { + allowSingleLine: true, + }], 'comma-dangle': ['error', { arrays: 'always-multiline', exports: 'always-multiline', @@ -22,13 +25,43 @@ export default [ imports: 'always-multiline', objects: 'always-multiline', }], + 'eqeqeq': ['error', 'always'], 'indent': ['error', 2, { - 'ignoreComments': true, - 'SwitchCase': 1, + ignoreComments: true, + SwitchCase: 1, + }], + 'keyword-spacing': ['error', { + before: true, + after: true, + overrides: { + catch: { before: true, after: true }, + }, }], + 'linebreak-style': [2, 'unix'], + 'no-debugger': 'error', + 'no-empty': 'off', + 'no-multiple-empty-lines': ['error', { + max: 1, + maxBOF: 0, + maxEOF: 0, + }], + 'no-new-wrappers': 'error', + 'no-prototype-builtins': 'error', + 'no-shadow-restricted-names': 'error', + 'no-throw-literal': 'error', + 'no-trailing-spaces': ['error'], + 'no-unsafe-optional-chaining': 'off', + 'no-useless-escape': 'off', + 'object-curly-spacing': ['error', 'always'], + 'padded-blocks': ['error', 'never'], 'quotes': ['error', 'single'], 'semi': ['error', 'never'], + 'space-infix-ops': ['error', { 'int32Hint': false }], + '@typescript-eslint/consistent-type-imports': ['error', { + prefer: 'type-imports', + fixStyle: 'separate-type-imports', + }], '@typescript-eslint/naming-convention': 'off', }, }, @@ -41,10 +74,41 @@ export default [ parserOptions: { parser: pluginTs.parser }, }, rules: { - 'vue/html-indent': ['error', 4], + 'vue/attributes-order': 'error', + 'vue/component-definition-name-casing': ['error', 'PascalCase'], + 'vue/component-name-in-template-casing': ['error', 'PascalCase'], + 'vue/first-attribute-linebreak': 'error', + 'vue/html-closing-bracket-newline': ['error', { + multiline: 'always', + singleline: 'never', + }], + 'vue/html-closing-bracket-spacing': 'error', + 'vue/html-indent': ['error', 4, { + attribute: 1, + closeBracket: 0, + alignAttributesVertically: true, + ignores: [], + }], + 'vue/html-self-closing': ['error', { + html: { + component: 'always', + normal: 'always', + void: 'always', + }, + math: 'always', + svg: 'always', + }], + 'vue/no-required-prop-with-default': 'error', + 'vue/max-attributes-per-line': ['error', { + singleline: 4, + multiline: { + max: 1, + }, + }], + 'vue/one-component-per-file': 'off', 'indent': ['error', 2, { ignoreComments: true, SwitchCase: 1 }], }, }, { ignores: ['dist/*'] }, { ignores: ['**/dist/*'] }, -] \ No newline at end of file +] diff --git a/index.d.ts b/index.d.ts index ada78497..f72ff0f6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,7 +9,7 @@ import type { RemoteCallable, } from '@remote-ui/rpc' -import { +import type { ContextAccessor, ContextSchema, CustomFieldKind, diff --git a/packages/v1-components/assets/stylesheets/motion.less b/packages/v1-components/assets/stylesheets/motion.less new file mode 100644 index 00000000..30866886 --- /dev/null +++ b/packages/v1-components/assets/stylesheets/motion.less @@ -0,0 +1 @@ +@transition: 0.25s ease; \ No newline at end of file diff --git a/packages/v1-components/assets/stylesheets/typography.less b/packages/v1-components/assets/stylesheets/typography.less index 2ff34301..0f835c38 100644 --- a/packages/v1-components/assets/stylesheets/typography.less +++ b/packages/v1-components/assets/stylesheets/typography.less @@ -8,6 +8,9 @@ @font-size-small: 14px; @font-size-tiny: 12px; +@font-weight-accent: 500; +@font-weight-normal: 400; + @line-height-h1: 44px; @line-height-h2: 32px; @line-height-h3: 32px; @@ -42,3 +45,9 @@ .text-tiny (@lh: (14/12)) { .text(400, @font-size-tiny, @lh); } .text-tiny-accent (@lh: (14/12)) { .text(500, @font-size-tiny, @lh); } + +.ellipsis () { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/packages/v1-components/src/common/components/menu.ts b/packages/v1-components/src/common/components/menu.ts new file mode 100644 index 00000000..af7a3166 --- /dev/null +++ b/packages/v1-components/src/common/components/menu.ts @@ -0,0 +1,30 @@ +export enum SIZE { + XS = 'xs', + SM = 'sm', + MD = 'md', + LG = 'lg', +} + +export type UiMenuItemProperties = { + description?: string; + counter?: string | number | null; + accent?: boolean; + active?: boolean; + danger?: boolean; + ticker?: boolean; + simple?: boolean; + size?: `${SIZE}`; + disabled?: boolean; +} + +type StyleDeclaration = CSSStyleDeclaration & { + '--delta-width': string +} + +export const deltaTransition = (el: HTMLElement): Partial => { + const delta = el.scrollWidth - el.clientWidth + return delta !== 0 ? { + '--delta-width': `-${delta}px`, + animationDuration: `${delta * 15}ms`, + } : {} +} diff --git a/packages/v1-components/src/common/components/select.ts b/packages/v1-components/src/common/components/select.ts new file mode 100644 index 00000000..c3449916 --- /dev/null +++ b/packages/v1-components/src/common/components/select.ts @@ -0,0 +1,110 @@ +import type { Alignment } from '@floating-ui/dom' +import type { PlacementOptions } from '@/common/components/popper' +import type { Side } from '@floating-ui/dom' +import type { Trigger } from '@/common/components/popper' +import type { TriggerSchema } from '@/common/components/popper' +import type { UiPopperProperties } from '@/common/components/popper' + +export enum SIZE { + XS = 'xs', + SM = 'sm', + XL = 'xl', +} + +export enum PLACEMENT { + TOP = 'top', + TOP_START = 'top-start', + TOP_END = 'top-end', + BOTTOM = 'bottom', + BOTTOM_START = 'bottom-start', + BOTTOM_END = 'bottom-end', + LEFT = 'left', + LEFT_START = 'left-start', +} + +export type Option = { + id: string; + value: unknown; + label: string; + isMatched (): boolean; +} + +export type UiSelectTriggerProperties = { + value?: unknown|unknown[]; + clearable?: boolean; + filter?: string; + invalid?: boolean; + multiple?: boolean; + expanded?: boolean; + placeholder?: string; + placeholderOnly?: boolean; + readonly?: boolean; + disabled?: boolean; + textboxSize?: SIZE | `${SIZE}`; +} + +export type UiSelectTriggerMethods = { + open (): void; + close (): void; + onClick (): void; + onInput (event: Event): void; + onFocus (event: Event): void; + onBlur (event: Event): void; + onClear (event: MouseEvent): void; +} + +export type UiSelectPopperProperties = { + opened?: boolean; + targetTriggers?: Trigger[] | TriggerSchema; + popperTriggers?: Trigger[] | TriggerSchema; + popperFitTrigger?: boolean; + placement?: Side | `${Side}-${Alignment}` | PlacementOptions; + popperClass?: string; + popperOptions?: Omit; + disabled?: boolean; + readonly?: boolean; + multiple?: boolean; + ticker?: boolean; +} + +export type UiSelectPopperMethods = { + autoScroll (): void; + updateWidth (): void; +} + +export type UiSelectOptionProperties = { + value: unknown | unknown[]; + label: string; + description?: string; + disabled?: boolean; + selected?: boolean; + multiple?: boolean; + active?: boolean; + size?: SIZE | `${SIZE}`; + counter?: string | number | null; + accent?: boolean; +} + +const escapeSpecialSymbols = (text: string): string => text.replace( + /([\\^$.*+?()[\]{}|=!<>:-])/g, + '\\$1' +) + +/** + * @param text Текст, в котором ищем слово + * @param term Подсвечиваемое слово + * @param style Стиль подсветки + * @return Исходный текст со вставками span тегов в местах, где встречается слово term + */ +export const highlight = (text: string, term: string, style: string): string => text.replace( + new RegExp(`(${escapeSpecialSymbols(term)})`, 'gi'), + `$1` +) + +let counter = 0 + +/** + * @param prefix + * @return Идентификатор, уникальный в рамках генерируемой функцией последовательности + */ +export const uid = (prefix = 'ui-v1-select') => `${prefix}-${++counter}` diff --git a/packages/v1-components/src/common/components/tag.ts b/packages/v1-components/src/common/components/tag.ts index 0ab249af..c268ecaa 100644 --- a/packages/v1-components/src/common/components/tag.ts +++ b/packages/v1-components/src/common/components/tag.ts @@ -22,7 +22,7 @@ type StyleDeclaration = CSSStyleDeclaration & { export function deltaTransition(el: HTMLElement): Partial { const delta = el.scrollWidth - el.clientWidth const animationDuration = `${((el.scrollWidth / el.clientWidth) * 2).toFixed(2)}s` - + return delta !== 0 ? { '--delta-width': `-${delta}px`, animationDuration, diff --git a/packages/v1-components/src/common/predicate.ts b/packages/v1-components/src/common/predicate.ts index d1ac1edd..eebd2cd5 100644 --- a/packages/v1-components/src/common/predicate.ts +++ b/packages/v1-components/src/common/predicate.ts @@ -13,6 +13,6 @@ export const isURL = (href: string, loose = true): boolean => { ].some(href => isURL(href, false)) } - return false + return false } } diff --git a/packages/v1-components/src/host.ts b/packages/v1-components/src/host.ts index 999a17f7..53e464d3 100644 --- a/packages/v1-components/src/host.ts +++ b/packages/v1-components/src/host.ts @@ -8,20 +8,24 @@ export { default as UiError } from '@/host/components/error/UiError.vue' export { default as UiImage } from '@/host/components/image/UiImage.vue' export { default as UiLink } from '@/host/components/link/UiLink.vue' export { default as UiLoader } from '@/host/components/loader/UiLoader.vue' +export { default as UiMenuItem } from '@/host/components/menu/UiMenuItem.vue' +export { default as UiMenuItemGroup } from '@/host/components/menu/UiMenuItemGroup.vue' export { default as UiModalSidebar } from '@/host/components/modal-sidebar/UiModalSidebar.vue' export { default as UiModalWindow } from '@/host/components/modal-window/UiModalWindow.vue' export { default as UiModalWindowSurface } from '@/host/components/modal-window/UiModalWindowSurface.vue' export { default as UiPopper } from '@/host/components/popper/UiPopper.vue' export { default as UiPopperConnector } from '@/host/components/popper/UiPopperConnector.vue' export { default as UiPopperTarget } from '@/host/components/popper/UiPopperTarget.vue' -export { default as UiTooltip } from '@/host/components/tooltip/UiTooltip.vue' export { default as UiRadio } from '@/host/components/radio/UiRadio.vue' export { default as UiScrollBox } from '@/host/components/scroll-box/UiScrollBox.vue' +export { default as UiSelectPopper } from '@/host/components/select/UiSelectPopper.vue' +export { default as UiSelectTrigger } from '@/host/components/select/UiSelectTrigger.vue' export { default as UiTag } from '@/host/components/tag/UiTag.vue' export { default as UiTextbox } from '@/host/components/textbox/UiTextbox.vue' export { default as UiToolbar } from '@/host/components/toolbar/UiToolbar.vue' export { default as UiToolbarButton } from '@/host/components/toolbar/UiToolbarButton.vue' export { default as UiToolbarLink } from '@/host/components/toolbar/UiToolbarLink.vue' +export { default as UiTooltip } from '@/host/components/tooltip/UiTooltip.vue' export { default as UiTransition } from '@/host/components/transition/UiTransition.vue' export { default as UiYandexMap } from '@/host/components/yandex-map/UiYandexMap.vue' diff --git a/packages/v1-components/src/host/components/avatar/injection.ts b/packages/v1-components/src/host/components/avatar/injection.ts index 9f5d9173..5cd51990 100644 --- a/packages/v1-components/src/host/components/avatar/injection.ts +++ b/packages/v1-components/src/host/components/avatar/injection.ts @@ -3,6 +3,6 @@ import type { ComputedRef, } from 'vue' -import { SIZE } from '@/common/components/avatar' +import type { SIZE } from '@/common/components/avatar' export const AvatarSizeKey = Symbol('UiAvatarSize') as InjectionKey> diff --git a/packages/v1-components/src/host/components/checkbox/UiCheckbox.vue b/packages/v1-components/src/host/components/checkbox/UiCheckbox.vue index d0e47545..fb50bb4b 100644 --- a/packages/v1-components/src/host/components/checkbox/UiCheckbox.vue +++ b/packages/v1-components/src/host/components/checkbox/UiCheckbox.vue @@ -20,7 +20,7 @@ type="checkbox" class="ui-v1-checkbox__input" @change="onChange" - > + /> + /> diff --git a/packages/v1-components/src/host/components/date/UiDate.vue.d.ts b/packages/v1-components/src/host/components/date/UiDate.vue.d.ts index d820a997..31fce10f 100644 --- a/packages/v1-components/src/host/components/date/UiDate.vue.d.ts +++ b/packages/v1-components/src/host/components/date/UiDate.vue.d.ts @@ -9,4 +9,3 @@ declare const UiDate: DefineComponent< > export default UiDate - diff --git a/packages/v1-components/src/host/components/image/UiImage.vue b/packages/v1-components/src/host/components/image/UiImage.vue index 790650dd..57630075 100644 --- a/packages/v1-components/src/host/components/image/UiImage.vue +++ b/packages/v1-components/src/host/components/image/UiImage.vue @@ -3,7 +3,7 @@ :alt="alt" :src="url" v-bind="$attrs" - > + /> + + diff --git a/packages/v1-components/storybook/stories/UiMenuItem.mdx b/packages/v1-components/storybook/stories/UiMenuItem.mdx new file mode 100644 index 00000000..82f13187 --- /dev/null +++ b/packages/v1-components/storybook/stories/UiMenuItem.mdx @@ -0,0 +1,44 @@ +import ToReact from '../ToReact.ts' + +import UiMenuItem from './UiMenuItem.example.vue' + +# UiMenuItem + +Используется для отображения элемента выпадающего списка (меню) + +## Механика работы +Содержит основной текст, может содержать иконку слева, справа или дополнительный текст + +## Только основной текст (Дефолтный слот) + + + +## Жирный текст со счетчиком справа + + + +## Основной текст и описание + + + +## Слот для аватара + + + +## Слот для аватара и слот для description + + + +## Иконка слева + + + +## Иконка справа + + +## Иконки слева и справа + + +## Уменьшенный размер (size="sm") + + diff --git a/packages/v1-components/storybook/stories/UiMenuItem.stories.ts b/packages/v1-components/storybook/stories/UiMenuItem.stories.ts new file mode 100644 index 00000000..76cc5d56 --- /dev/null +++ b/packages/v1-components/storybook/stories/UiMenuItem.stories.ts @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/vue3' + +import UiMenuItem from '@/host/components/menu/UiMenuItem.vue' + +import page from './UiMenuItem.mdx' + +import { SIZE } from '@/common/components/menu' + +const meta = { + id: 'UiMenuItem', + + title: 'Components/UiMenuItem', + + component: UiMenuItem, + + argTypes: { + size: { + options: Object.values(SIZE), + }, + + counter: { + control: { type: 'number' }, + }, + }, + + render: (args) => ({ + components: { + UiMenuItem, + }, + + setup: () => ({ args }), + + template: ` + + Audrey Robertson + + `, + }), + + parameters: { + docs: { page }, + layout: 'centered', + }, +} satisfies Meta + +// noinspection JSUnusedGlobalSymbols +export default meta + +type Story = StoryObj + +export const Sandbox: Story = { + args: { + size: 'md', + description: '', + counter: 0, + accent: true, + active: false, + danger: false, + }, +} \ No newline at end of file diff --git a/packages/v1-components/storybook/stories/UiModalWindow.stories.ts b/packages/v1-components/storybook/stories/UiModalWindow.stories.ts index 62576115..d8c970bc 100644 --- a/packages/v1-components/storybook/stories/UiModalWindow.stories.ts +++ b/packages/v1-components/storybook/stories/UiModalWindow.stories.ts @@ -34,7 +34,7 @@ const meta = { control: 'select', options: Object.values(APPEARANCE), }, - + scrolling: { control: 'select', options: Object.values(SCROLLING), @@ -44,20 +44,20 @@ const meta = { default: { control: false }, footer: { control: false }, }, - + render: (args: unknown) => ({ components: { UiButton, UiModalWindow, }, - + setup () { return { args, open: ref(false), } }, - + template: ` Открыть diff --git a/packages/v1-components/storybook/stories/UiSelect.mdx b/packages/v1-components/storybook/stories/UiSelect.mdx new file mode 100644 index 00000000..b8b98bf9 --- /dev/null +++ b/packages/v1-components/storybook/stories/UiSelect.mdx @@ -0,0 +1,119 @@ +import { ArgTypes } from '@storybook/addon-docs/blocks' + +# UiSelect + +`UiSelect` - выпадающий список для выбора одного или нескольких значений. +Компонент собирается из `UiSelect` + `UiSelectOption` (и при необходимости `UiSelectOptionGroup`). + +## Когда использовать + +* Нужно выбрать одно значение из набора (статус, исполнитель, категория). +* Нужен множественный выбор (`multiple`) с отображением количества выбранных элементов. +* Нужен поиск по опциям (`filterable`), включая подсветку совпадений в `UiSelectOption`. + +## Базовое применение + +```html + + + +``` + +## Множественный выбор + +Для множественного режима включите `multiple`. + +```html + +``` + +## Поиск по опциям + +Чтобы включить поиск, передайте `filterable`. +В этом режиме ввод в триггере используется как строка фильтра, а опции автоматически скрываются/подсвечиваются по совпадению. + +```html + +``` + +## Опции как объекты + +Если `value` у опций - объекты, задайте `equalsFn`, чтобы корректно сравнивать значения. + +```html + +``` + +## Группировка опций + +Для групп используйте `UiSelectOptionGroup`. + +```html + + + +``` + +## Важные свойства + +* `placeholderOnly` - всегда отображает `placeholder` в поле, вместо текста выбранных опций. +* `textboxSize` - размер текстового поля триггера (`xs`, `sm`, `xl`). +* `expanded` - внешний флаг открытия/закрытия списка. +* `placement`, `targetTriggers`, `popperTriggers`, `popperFitTrigger`, `popperOptions` - настройка поведения и позиционирования выпадающего списка. + +## Слоты + +* `default` - содержимое выпадающего списка (обычно `UiSelectOption` и `UiSelectOptionGroup`). + +## API + + diff --git a/packages/v1-components/storybook/stories/UiSelect.remote.ts b/packages/v1-components/storybook/stories/UiSelect.remote.ts new file mode 100644 index 00000000..f86e3c9e --- /dev/null +++ b/packages/v1-components/storybook/stories/UiSelect.remote.ts @@ -0,0 +1,73 @@ +import { UiSelect } from '../../src/remote/components/select' +import { UiSelectOption } from '../../src/remote/components/select' + +import { createComponentEndpoint } from '../endpoint' +import { h } from 'vue' + +import IconNotification from '../../assets/sprites/alerts/notifications.svg' +import IconWarning from '../../assets/sprites/alerts/warning.svg' +import IconError from '../../assets/sprites/alerts/error-outlined.svg' + +type UiSelectProps = InstanceType['$props'] + +createComponentEndpoint({ + async run (createApp, root, props) { + const app = createApp({ + setup ( ) { + return () => h(UiSelect, { + ...props, + equalsFn: (a: unknown, b: unknown) => a === b, + },{ + default: () => + [ + 'Kyle Simmmons', + 'Eduardo Henry', + 'Philip Williamson', + 'Max Miles', + 'Caroline Allen', + 'Joanne Thompson Joanne Thompson Joanne Thompson Thompson Thompson', + 'DAD', + ].map(t => + h(UiSelectOption, { + value: t, + label: t, + }, { + 'leading-icon': ({ selected }: { selected: boolean }) => { + if (selected) { + return h(IconWarning, { + class: 'ui-v1-select-option__checkmark-icon', + 'aria-hidden': 'true', + }) + } + + return null + }, + + 'trailing-icon': ({ selected }: { selected: boolean }) => { + if (selected) { + return h(IconNotification, { + class: 'ui-v1-select-option__checkmark-icon', + 'aria-hidden': 'true', + }) + } + + if (props.multiple) { + return h(IconError, { + class: 'ui-v1-select-option__add-icon', + 'aria-hidden': 'true', + }) + } + + return null + }, + }) + ), + }) + }, + }) + + app.mount(root) + + return () => app.unmount() + }, +}, self as unknown as Worker) diff --git a/packages/v1-components/storybook/stories/UiSelect.stories.ts b/packages/v1-components/storybook/stories/UiSelect.stories.ts new file mode 100644 index 00000000..e288b95e --- /dev/null +++ b/packages/v1-components/storybook/stories/UiSelect.stories.ts @@ -0,0 +1,120 @@ +import type { Callable } from '../endpoint' +import type { Lifecycle } from '../endpoint' +import type { Meta } from '@storybook/vue3' +import type { StoryObj } from '@storybook/vue3' + +import UiMenuItem from '@/host/components/menu/UiMenuItem.vue' +import UiMenuItemGroup from '@/host/components/menu/UiMenuItemGroup.vue' +import UiPopperConnector from '@/host/components/popper/UiPopperConnector.vue' +import UiSelectPopper from '@/host/components/select/UiSelectPopper.vue' +import UiSelectTrigger from '@/host/components/select/UiSelectTrigger.vue' + +import { UiSelect } from '../../src/remote/components/select' + +import { HostedTree } from '@omnicajs/vue-remote/host' +import { createEndpoint } from '@remote-ui/rpc' +import { createProvider } from '@omnicajs/vue-remote/host' +import { createReceiver } from '@omnicajs/vue-remote/host' +import { watch } from 'vue' + +import page from './UiSelect.mdx' + +import { SIZE } from '@/common/components/select' +import { PLACEMENT } from '@/common/components/select' + +const provider = createProvider({ + UiMenuItem, + UiMenuItemGroup, + UiPopperConnector, + UiSelectPopper, + UiSelectTrigger, +}) +const receiver = createReceiver() + +const meta = { + title: 'Components/UiSelect', + + component: UiSelect, + + argTypes: { + id: { control: false }, + value: { control: false }, + expanded: { control: 'boolean' }, + clearable: { control: 'boolean' }, + placeholder: { control: 'text' }, + filterable: { control: 'boolean' }, + invalid: { control: 'boolean' }, + placeholderOnly: { control: 'boolean' }, + readonly: { control: 'boolean' }, + disabled: { control: 'boolean' }, + multiple: { control: 'boolean' }, + ticker: { control: 'boolean' }, + + textboxSize: { + options: Object.values(SIZE), + }, + + placement: { + options: Object.values(PLACEMENT), + }, + + popperFitTrigger: { control: 'boolean' }, + popperClass: { control: 'text' }, + popperOptions: { control: false }, + targetTriggers: { control: false }, + popperTriggers: { control: false }, + }, + + render: (args) => ({ + components: { + HostedTree, + UiMenuItem, + UiMenuItemGroup, + UiPopperConnector, + UiSelectPopper, + UiSelectTrigger, + }, + + setup () { + const worker = new Worker(new URL('./UiSelect.remote.ts', import.meta.url), { type: 'module' }) + + const endpoint = createEndpoint(worker) + + endpoint.call.run(receiver.receive, args) + + watch(args, (newArgs) => { + endpoint.call.setProps(newArgs) + }) + + return { + args, + provider, + receiver, + } + }, + + template: ` + + `, + }), + + parameters: { + docs: { page }, + layout: 'centered', + }, +} satisfies Meta + +// noinspection JSUnusedGlobalSymbols +export default meta + +type Story = StoryObj; + +export const Sandbox: Story = { + args: { + placeholder: 'test123', + ticker: false, + disabled: false, + multiple: false, + filterable: false, + }, +} diff --git a/packages/v1-components/storybook/tsconfig.json b/packages/v1-components/storybook/tsconfig.json index 856762ce..4862b74a 100644 --- a/packages/v1-components/storybook/tsconfig.json +++ b/packages/v1-components/storybook/tsconfig.json @@ -2,6 +2,10 @@ "extends": "../tsconfig.json", "include": [ "storybook/**/*.stories.ts", - "storybook/**/*.vue" + "storybook/**/*.vue", + "stories/**/*.ts", + "stories/**/*.vue", + "stories/**/*.mdx", + "./../shims-*.d.ts", ] } diff --git a/packages/v1-contexts/src/common/order/card.ts b/packages/v1-contexts/src/common/order/card.ts index bce524d3..893c415f 100644 --- a/packages/v1-contexts/src/common/order/card.ts +++ b/packages/v1-contexts/src/common/order/card.ts @@ -233,7 +233,7 @@ export const description: ContextSchemaDescription = { description: { 'en-GB': 'The order ID in the external source system', 'es-ES': 'El ID del pedido en el sistema de origen externo', - 'ru-RU': 'Идентифкатор заказа во внешней системе источника', + 'ru-RU': 'Идентификатор заказа во внешней системе источника', }, }, 'type': { diff --git a/packages/v1-contexts/src/common/settings.ts b/packages/v1-contexts/src/common/settings.ts index 782b7b97..c66d788c 100644 --- a/packages/v1-contexts/src/common/settings.ts +++ b/packages/v1-contexts/src/common/settings.ts @@ -10,7 +10,7 @@ import type { Schema, } from '~types/settings' -import { +import type { Route, RoutesMap, RoutingData, diff --git a/packages/v1-contexts/tests/__util__/createAccessor.ts b/packages/v1-contexts/tests/__util__/createAccessor.ts index a122f2b9..62df3408 100644 --- a/packages/v1-contexts/tests/__util__/createAccessor.ts +++ b/packages/v1-contexts/tests/__util__/createAccessor.ts @@ -1,4 +1,4 @@ -import { +import type { ContextSchema, FieldAccessor, FieldGetters, diff --git a/packages/v1-contexts/tests/predicates.test-d.ts b/packages/v1-contexts/tests/predicates.test-d.ts new file mode 100644 index 00000000..be744b7b --- /dev/null +++ b/packages/v1-contexts/tests/predicates.test-d.ts @@ -0,0 +1,132 @@ +import { + isNumber, + isShape, + isString, +} from '@/predicates' +import { + assertType, + describe, + expectTypeOf, + it, + test, +} from 'vitest' + +type InferShape = S extends (value: unknown) => value is infer U + ? U + : never + +describe('isShape type inference', () => { + test('infers required fields', () => { + const isMoney = isShape({ + amount: [isNumber, true], + currency: [isString, true], + }) + + assertType<(value: unknown) => value is { + amount: number; + currency: string; + }>(isMoney) + + type Money = InferShape + + expectTypeOf().toEqualTypeOf<{ + amount: number; + currency: string; + }>() + + assertType({ + amount: 100, + currency: 'USD', + }) + }) + + it('keeps optional field as T | undefined and not as optional key', () => { + const isProductWhereNameIsOptional = isShape({ + id: [isNumber, true], + name: [isString, false], + }) + + assertType<(value: unknown) => value is { + id: number; + name: string | undefined; + }>(isProductWhereNameIsOptional) + + type ProductWithOptionalName = InferShape + + expectTypeOf().toEqualTypeOf<{ + id: number; + name: string | undefined; + }>() + + assertType({ + id: 1, + name: undefined, + }) + + // @ts-expect-error Current inference model marks optional fields as `T | undefined`, but not optional keys. + assertType({ + id: 1, + }) + }) + + test('infers nested shapes and spread-based composition', () => { + const isMoney = isShape({ + amount: [isNumber, true], + currency: [isString, true], + }) + + const isPrice = isShape({ + ...isMoney.shape, + type: [isShape({ + id: [isNumber, true], + code: [isString, false], + }), true], + }) + + type Price = InferShape + + expectTypeOf().toEqualTypeOf<{ + amount: number; + currency: string; + type: { + id: number; + code: string | undefined; + }; + }>() + + const isProduct = isShape({ + id: [isNumber, true], + name: [isString, true], + price: [isPrice, true], + cost: [isMoney, true], + }) + + type Money = InferShape + + assertType<(value: unknown) => value is { + id: number; + name: string; + price: Price; + cost: Money; + }>(isProduct) + + type Product = InferShape + + expectTypeOf().toEqualTypeOf<{ + id: number; + name: string; + price: { + amount: number; + currency: string; + type: { + id: number; + code: string | undefined; + }; + }; + cost: { + amount: number; + currency: string; + }; + }>() + }) +}) diff --git a/skills/commit-workflow/SKILL.md b/skills/commit-workflow/SKILL.md new file mode 100644 index 00000000..1e360a8b --- /dev/null +++ b/skills/commit-workflow/SKILL.md @@ -0,0 +1,77 @@ +--- +name: commit-workflow +description: Use this skill when creating commits in this repository. It standardizes commit splitting, Conventional Commit type/scope selection, and English commit messages with workspace-folder scopes. +--- + +# Commit Workflow + +## When To Use +Use this skill when the user asks to: +- create one or more commits; +- split changes into multiple commits; +- choose commit message types/scopes; +- validate commit format before committing. + +## Source Of Truth +- `AGENTS.md` +- root `package.json` (`workspaces: ["packages/*"]`) +- actual workspace folders under `packages/` + +## Required Rules +- Commit format: Conventional Commits. +- Commit message language: English. +- Allowed types: `feat`, `fix`, `build`, `ci`, `perf`, `docs`, `refactor`, `style`, `test`, `chore`. +- Scope rule for workspace changes: use workspace folder name (not npm package name). + - Current workspace scopes: + - `v1-components` + - `v1-contexts` + - `v1-testing` + - `v1-types` +- For root/global repository changes, scope may be omitted. +- Split commits by logical intent. +- If one change affects multiple workspaces, split by workspace unless user explicitly asks to combine. +- Keep subject concise and factual. +- Start subject description with an uppercase letter. +- Mention affected component(s) or area in subject description when applicable. +- Subject should describe completed change in past tense. +- Prefer passive voice for changelog-friendly phrasing. +- Do not amend/rewrite history unless explicitly requested. + +## Workflow +1. Inspect pending changes: +```bash +git status --short +git diff +``` +2. Map changed files to workspace folders (`packages//...`) or root/global files. +3. Group changes into commit batches by logical intent and workspace boundary. +4. Choose commit header: +```text +(): +``` +or for global changes: +```text +: +``` +5. Stage only files for the current batch: +```bash +git add +``` +6. Create commit (non-interactive): +```bash +git commit -m "(): " +``` +7. Verify result: +```bash +git show --name-status --oneline -n 1 +``` + +## Practical Patterns +- Workspace feature: +`feat(v1-components): UiSelect searchable select option groups added` +- Workspace fix: +`fix(v1-contexts): OrderContext missing order id handling corrected` +- Global docs update: +`docs: AGENTS repository agent instructions updated` +- Root tooling/config change: +`chore: Root yarn workspace build flow updated` diff --git a/skills/sync-remote-host-registry/SKILL.md b/skills/sync-remote-host-registry/SKILL.md new file mode 100644 index 00000000..35402e32 --- /dev/null +++ b/skills/sync-remote-host-registry/SKILL.md @@ -0,0 +1,62 @@ +--- +name: sync-remote-host-registry +description: Use this skill when updating remote-to-host component registries (for example createRoot component lists) to keep them aligned with the actual render graph instead of naive host export parity. +--- + +# Sync Remote Host Registry + +## When To Use +Use this skill when: +- updating `createRoot(..., { components: [...] })` lists; +- updating Storybook provider/receiver setups for remote rendering; +- checking whether a host component should be added to or removed from a remote render registry. + +## Source Of Truth +- `src/index.ts` (`createRoot` in root package) +- `packages/v1-components/src/host.ts` +- `packages/v1-components/src/remote.ts` +- `packages/v1-components/src/remote/components/**/*` +- `packages/v1-components/storybook/endpoint.ts` +- `packages/v1-components/storybook/stories/UiSelect.stories.ts` +- `packages/v1-components/storybook/stories/UiSelect.remote.ts` + +## Core Model +- `host.ts` is the catalog of available host implementations. +- `remote.ts` is the public remote API surface. +- Registries like `createRoot(...components)` must include host components that are schema-reachable from remote rendering flow. +- Non-1:1 mapping is valid: + - one remote component may render through multiple host primitives; + - host-only components can exist and must not be added if remote flow never emits them. + +## Practical Rules +- Do not assume parity with `host.ts`. +- Include a host component if remote rendering can emit its schema in this runtime path. +- Exclude host-only components that are not emitted by remote instructions in this runtime path. +- For Storybook remote stories, provider can stay minimal and story-specific. +- For product/runtime `src/index.ts`, registry should cover all host schemas used by runtime remote render graph. + +## Workflow +1. Inspect current registry and target area: +```bash +nl -ba src/index.ts | sed -n '68,120p' +``` +2. Inspect host export catalog: +```bash +nl -ba packages/v1-components/src/host.ts | sed -n '1,120p' +``` +3. Inspect remote public surface: +```bash +nl -ba packages/v1-components/src/remote.ts | sed -n '1,120p' +``` +4. Inspect composite remote components (especially non-1:1 cases) and their internal parts. +5. Update registry by render-graph reachability, not by export parity. +6. Validate: +```bash +yarn eslint src/index.ts +``` + +## Review Checklist +- Every newly added registry item is justified by remote render flow. +- Removed items are proven host-only for this runtime path. +- Composite components (like select) are checked through their internal remote parts. +- Storybook-specific minimal providers are not blindly copied into runtime and vice versa. diff --git a/skills/yarn-lock-conflict-resolution/SKILL.md b/skills/yarn-lock-conflict-resolution/SKILL.md new file mode 100644 index 00000000..4e7c7d6d --- /dev/null +++ b/skills/yarn-lock-conflict-resolution/SKILL.md @@ -0,0 +1,76 @@ +--- +name: yarn-lock-conflict-resolution +description: Use this skill when resolving merge/rebase conflicts in yarn.lock. It standardizes taking yarn.lock from the target branch, reconciling it with yarn install, and reusing a successful resolution across repeated conflict rounds. +--- + +# Yarn.lock Conflict Resolution + +## When To Use +Use this skill when: +- `yarn.lock` has merge/rebase conflicts; +- conflict resolution should be repeatable and low-risk; +- the same conflict appears in multiple rounds of one rebase/merge sequence. + +## Source Of Truth Policy +- Rebase: take `yarn.lock` from the branch you rebase onto. +- Merge: take `yarn.lock` from `HEAD` (current target branch). +- After taking baseline, run `yarn install` to reconcile lock metadata with current manifests. + +## Required Rules +- Do not manually resolve conflict markers inside `yarn.lock`. +- Replace `yarn.lock` completely from the selected baseline. +- Reuse previous successful resolution for repeated rounds in the same sequence. +- If dependency updates were intentional in the rebased commit, replay dependency commands after conflict resolution. + +## Workflow +1. Ensure conflict exists: +```bash +git status --short +``` +2. (Optional) Ensure Yarn config exists: +```bash +make .yarnrc.yml +``` +3. Resolve first conflict round for rebase: +```bash +ONTO=$(cat .git/rebase-merge/onto 2>/dev/null || cat .git/rebase-apply/onto) +git show "$ONTO:yarn.lock" > yarn.lock +yarn install +git add yarn.lock +cp yarn.lock .git/yarn-lock-resolution-base +``` +4. Resolve first conflict round for merge: +```bash +git show "HEAD:yarn.lock" > yarn.lock +yarn install +git add yarn.lock +cp yarn.lock .git/yarn-lock-resolution-base +``` +5. Resolve repeated rounds in the same sequence: +```bash +cp .git/yarn-lock-resolution-base yarn.lock +yarn install +git add yarn.lock +cp yarn.lock .git/yarn-lock-resolution-base +``` +6. Continue operation: +```bash +git rebase --continue +# or +git merge --continue +``` +7. Cleanup after finish: +```bash +rm -f .git/yarn-lock-resolution-base +``` +8. If dependency updates must be replayed, run original dependency command and commit lockfile update with an English Conventional Commit message, for example: +```bash +yarn up +git add yarn.lock +git commit -m "chore: refresh yarn.lock" +``` + +## Validation +- `git status --short` has no unresolved conflicts for `yarn.lock`. +- `yarn.lock` is staged before `--continue`. +- Resolution follows source-of-truth policy above. diff --git a/src/composables.ts b/src/composables.ts index 7d081036..5e927ca6 100644 --- a/src/composables.ts +++ b/src/composables.ts @@ -2,7 +2,7 @@ import type { Callable } from '~types/host/callable' import type { RouteParams } from '@omnicajs/symfony-router' -import { +import type { Context, ContextAccessor, ContextSchema, diff --git a/src/index.ts b/src/index.ts index 9ed494a2..9e7f9650 100644 --- a/src/index.ts +++ b/src/index.ts @@ -81,6 +81,8 @@ const createRoot = async (channel: Channel) => { 'UiImage', 'UiLink', 'UiLoader', + 'UiMenuItem', + 'UiMenuItemGroup', 'UiModalSidebar', 'UiModalWindow', 'UiModalWindowSurface', @@ -89,6 +91,8 @@ const createRoot = async (channel: Channel) => { 'UiPopperTarget', 'UiRadio', 'UiScrollBox', + 'UiSelectPopper', + 'UiSelectTrigger', 'UiTag', 'UiTextbox', 'UiToolbarButton', diff --git a/tests/__util__/createAccessor.ts b/tests/__util__/createAccessor.ts index 3c62fa27..54d88320 100644 --- a/tests/__util__/createAccessor.ts +++ b/tests/__util__/createAccessor.ts @@ -1,4 +1,4 @@ -import { +import type { ContextSchema, FieldAccessor, FieldGetters,