@aleph-alpha/settings-kit 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +465 -15
  2. package/dist/SkLayout.vue_vue_type_script_setup_true_lang-ZjV2AMdR.js +51 -0
  3. package/dist/SkLayoutPage-DGbCpQFm.js +2545 -0
  4. package/dist/UseFileConvertor-DN6cCXNi.js +50 -0
  5. package/dist/components/SkField/SkField.vue.d.ts +28 -0
  6. package/dist/components/SkField/SkField.vue.d.ts.map +1 -0
  7. package/dist/components/SkField/index.d.ts +2 -0
  8. package/dist/components/SkField/index.d.ts.map +1 -0
  9. package/dist/components/SkGroup/SkGroup.vue.d.ts +29 -0
  10. package/dist/components/SkGroup/SkGroup.vue.d.ts.map +1 -0
  11. package/dist/components/SkGroup/index.d.ts +2 -0
  12. package/dist/components/SkGroup/index.d.ts.map +1 -0
  13. package/dist/components/SkItem/SkItem.vue.d.ts +22 -0
  14. package/dist/components/SkItem/SkItem.vue.d.ts.map +1 -0
  15. package/dist/components/SkItem/index.d.ts +2 -0
  16. package/dist/components/SkItem/index.d.ts.map +1 -0
  17. package/dist/components/SkLayout/SkLayout.vue.d.ts +53 -0
  18. package/dist/components/SkLayout/SkLayout.vue.d.ts.map +1 -0
  19. package/dist/components/SkLayout/SkLayoutPage.d.ts +23 -0
  20. package/dist/components/SkLayout/SkLayoutPage.d.ts.map +1 -0
  21. package/dist/components/SkLayout/context.d.ts +61 -0
  22. package/dist/components/SkLayout/context.d.ts.map +1 -0
  23. package/dist/components/SkLayout/index.d.ts +5 -0
  24. package/dist/components/SkLayout/index.d.ts.map +1 -0
  25. package/dist/components/SkPage/SkPage.vue.d.ts +35 -0
  26. package/dist/components/SkPage/SkPage.vue.d.ts.map +1 -0
  27. package/dist/components/SkPage/index.d.ts +2 -0
  28. package/dist/components/SkPage/index.d.ts.map +1 -0
  29. package/dist/components/SkSection/SkSection.vue.d.ts +29 -0
  30. package/dist/components/SkSection/SkSection.vue.d.ts.map +1 -0
  31. package/dist/components/SkSection/index.d.ts +2 -0
  32. package/dist/components/SkSection/index.d.ts.map +1 -0
  33. package/dist/components/controls/SkActionsControl/SkActionsControl.vue.d.ts +37 -0
  34. package/dist/components/controls/SkActionsControl/SkActionsControl.vue.d.ts.map +1 -0
  35. package/dist/components/controls/SkActionsControl/index.d.ts +2 -0
  36. package/dist/components/controls/SkActionsControl/index.d.ts.map +1 -0
  37. package/dist/components/controls/SkCheckboxControl/SkCheckboxControl.vue.d.ts +35 -0
  38. package/dist/components/controls/SkCheckboxControl/SkCheckboxControl.vue.d.ts.map +1 -0
  39. package/dist/components/controls/SkCheckboxControl/index.d.ts +2 -0
  40. package/dist/components/controls/SkCheckboxControl/index.d.ts.map +1 -0
  41. package/dist/components/controls/SkColorControl/SkColorControl.vue.d.ts +35 -0
  42. package/dist/components/controls/SkColorControl/SkColorControl.vue.d.ts.map +1 -0
  43. package/dist/components/controls/SkColorControl/index.d.ts +2 -0
  44. package/dist/components/controls/SkColorControl/index.d.ts.map +1 -0
  45. package/dist/components/controls/SkFileUpload/SkFilePlaceholder.vue.d.ts +11 -0
  46. package/dist/components/controls/SkFileUpload/SkFilePlaceholder.vue.d.ts.map +1 -0
  47. package/dist/components/controls/SkFileUpload/SkFileUpload.vue.d.ts +46 -0
  48. package/dist/components/controls/SkFileUpload/SkFileUpload.vue.d.ts.map +1 -0
  49. package/dist/components/controls/SkFileUpload/index.d.ts +2 -0
  50. package/dist/components/controls/SkFileUpload/index.d.ts.map +1 -0
  51. package/dist/components/controls/SkPeriodControl/SkPeriodControl.vue.d.ts +35 -0
  52. package/dist/components/controls/SkPeriodControl/SkPeriodControl.vue.d.ts.map +1 -0
  53. package/dist/components/controls/SkPeriodControl/index.d.ts +2 -0
  54. package/dist/components/controls/SkPeriodControl/index.d.ts.map +1 -0
  55. package/dist/components/controls/SkSelectControl/SkSelectControl.vue.d.ts +40 -0
  56. package/dist/components/controls/SkSelectControl/SkSelectControl.vue.d.ts.map +1 -0
  57. package/dist/components/controls/SkSelectControl/index.d.ts +2 -0
  58. package/dist/components/controls/SkSelectControl/index.d.ts.map +1 -0
  59. package/dist/components/controls/SkTelControl/SKTelPrefixControl.vue.d.ts +16 -0
  60. package/dist/components/controls/SkTelControl/SKTelPrefixControl.vue.d.ts.map +1 -0
  61. package/dist/components/controls/SkTelControl/SkTelControl.vue.d.ts +35 -0
  62. package/dist/components/controls/SkTelControl/SkTelControl.vue.d.ts.map +1 -0
  63. package/dist/components/controls/SkTelControl/index.d.ts +4 -0
  64. package/dist/components/controls/SkTelControl/index.d.ts.map +1 -0
  65. package/dist/components/controls/SkTelControl/prefixes.d.ts +26 -0
  66. package/dist/components/controls/SkTelControl/prefixes.d.ts.map +1 -0
  67. package/dist/components/controls/SkTextControl/SkTextControl.vue.d.ts +45 -0
  68. package/dist/components/controls/SkTextControl/SkTextControl.vue.d.ts.map +1 -0
  69. package/dist/components/controls/SkTextControl/index.d.ts +2 -0
  70. package/dist/components/controls/SkTextControl/index.d.ts.map +1 -0
  71. package/dist/components/controls/SkUnsupportedControl/SkUnsupportedControl.vue.d.ts +8 -0
  72. package/dist/components/controls/SkUnsupportedControl/SkUnsupportedControl.vue.d.ts.map +1 -0
  73. package/dist/components/controls/storyEventLog.d.ts +33 -0
  74. package/dist/components/controls/storyEventLog.d.ts.map +1 -0
  75. package/dist/components/index.d.ts +7 -2
  76. package/dist/components/index.d.ts.map +1 -1
  77. package/dist/components/index.js +12 -2
  78. package/dist/composables/UseFileConvertor/UseFileConvertor.d.ts +7 -0
  79. package/dist/composables/UseFileConvertor/UseFileConvertor.d.ts.map +1 -0
  80. package/dist/composables/index.d.ts +14 -0
  81. package/dist/composables/index.d.ts.map +1 -0
  82. package/dist/composables/useFieldInheritance/useFieldInheritance.d.ts +24 -0
  83. package/dist/composables/useFieldInheritance/useFieldInheritance.d.ts.map +1 -0
  84. package/dist/composables/useFieldValidator/useFieldValidator.d.ts +50 -0
  85. package/dist/composables/useFieldValidator/useFieldValidator.d.ts.map +1 -0
  86. package/dist/composables/useSettingField/useSettingField.d.ts +24 -0
  87. package/dist/composables/useSettingField/useSettingField.d.ts.map +1 -0
  88. package/dist/composables/useSettingsForm/useSettingsForm.d.ts +12 -0
  89. package/dist/composables/useSettingsForm/useSettingsForm.d.ts.map +1 -0
  90. package/dist/composables/useSettingsLayout/useSettingsLayout.d.ts +71 -0
  91. package/dist/composables/useSettingsLayout/useSettingsLayout.d.ts.map +1 -0
  92. package/dist/composables/useSettingsLocale/useSettingsLocale.d.ts +38 -0
  93. package/dist/composables/useSettingsLocale/useSettingsLocale.d.ts.map +1 -0
  94. package/dist/composables/useStateManager/useStateManager.d.ts +10 -0
  95. package/dist/composables/useStateManager/useStateManager.d.ts.map +1 -0
  96. package/dist/core/controls/controlDefinitions.d.ts +33 -0
  97. package/dist/core/controls/controlDefinitions.d.ts.map +1 -0
  98. package/dist/core/controls/index.d.ts +26 -0
  99. package/dist/core/controls/index.d.ts.map +1 -0
  100. package/dist/core/index.d.ts +18 -0
  101. package/dist/core/index.d.ts.map +1 -0
  102. package/dist/core/layout/index.d.ts +8 -0
  103. package/dist/core/layout/index.d.ts.map +1 -0
  104. package/dist/core/layout/navigation.d.ts +33 -0
  105. package/dist/core/layout/navigation.d.ts.map +1 -0
  106. package/dist/core/layout/resolve.d.ts +41 -0
  107. package/dist/core/layout/resolve.d.ts.map +1 -0
  108. package/dist/core/layout/types.d.ts +160 -0
  109. package/dist/core/layout/types.d.ts.map +1 -0
  110. package/dist/core/layout/validate.d.ts +26 -0
  111. package/dist/core/layout/validate.d.ts.map +1 -0
  112. package/dist/core/types.d.ts +541 -0
  113. package/dist/core/types.d.ts.map +1 -0
  114. package/dist/core/useControlModel.d.ts +13 -0
  115. package/dist/core/useControlModel.d.ts.map +1 -0
  116. package/dist/core/utils/a11y.d.ts +25 -0
  117. package/dist/core/utils/a11y.d.ts.map +1 -0
  118. package/dist/core/utils/cast.d.ts +8 -0
  119. package/dist/core/utils/cast.d.ts.map +1 -0
  120. package/dist/core/utils/consts.d.ts +978 -0
  121. package/dist/core/utils/consts.d.ts.map +1 -0
  122. package/dist/core/utils/kitMessages.d.ts +165 -0
  123. package/dist/core/utils/kitMessages.d.ts.map +1 -0
  124. package/dist/core/utils/localizedText.d.ts +10 -0
  125. package/dist/core/utils/localizedText.d.ts.map +1 -0
  126. package/dist/core/utils/useName.d.ts +7 -0
  127. package/dist/core/utils/useName.d.ts.map +1 -0
  128. package/dist/core/validate/constraints.d.ts +20 -0
  129. package/dist/core/validate/constraints.d.ts.map +1 -0
  130. package/dist/core/validate/rules.d.ts +29 -0
  131. package/dist/core/validate/rules.d.ts.map +1 -0
  132. package/dist/index-BQ6z6MK4.js +214 -0
  133. package/dist/index-BXs-0JdT.js +168 -0
  134. package/dist/index-BynIH0EE.js +92 -0
  135. package/dist/index-C-WLhRod.js +100 -0
  136. package/dist/index-CFMv8jSO.js +4 -0
  137. package/dist/index-CGYERet5.js +373 -0
  138. package/dist/index-KAbhkO1J.js +81 -0
  139. package/dist/index.d.ts +8 -2
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +48 -3
  142. package/dist/service/index.d.ts +2 -0
  143. package/dist/service/index.d.ts.map +1 -0
  144. package/dist/shell/SkShell.vue.d.ts +80 -0
  145. package/dist/shell/SkShell.vue.d.ts.map +1 -0
  146. package/dist/shell/SkShellFrame.vue.d.ts +46 -0
  147. package/dist/shell/SkShellFrame.vue.d.ts.map +1 -0
  148. package/dist/shell/SkShellNav.vue.d.ts +19 -0
  149. package/dist/shell/SkShellNav.vue.d.ts.map +1 -0
  150. package/dist/shell/history.d.ts +9 -0
  151. package/dist/shell/history.d.ts.map +1 -0
  152. package/dist/shell/index.d.ts +12 -0
  153. package/dist/shell/index.d.ts.map +1 -0
  154. package/dist/shell/index.js +848 -0
  155. package/dist/shell/labels.d.ts +25 -0
  156. package/dist/shell/labels.d.ts.map +1 -0
  157. package/dist/shell/location.d.ts +18 -0
  158. package/dist/shell/location.d.ts.map +1 -0
  159. package/dist/shell/routes.d.ts +19 -0
  160. package/dist/shell/routes.d.ts.map +1 -0
  161. package/dist/shell/useMediaQuery.d.ts +4 -0
  162. package/dist/shell/useMediaQuery.d.ts.map +1 -0
  163. package/dist/shell/useShellLocation.d.ts +37 -0
  164. package/dist/shell/useShellLocation.d.ts.map +1 -0
  165. package/package.json +42 -23
  166. package/dist/SettingToggleUnit.vue_vue_type_script_setup_true_lang-BM7-6aXj.js +0 -61
  167. package/dist/components/SettingToggleUnit/SettingToggleUnit.vue.d.ts +0 -16
  168. package/dist/components/SettingToggleUnit/SettingToggleUnit.vue.d.ts.map +0 -1
  169. package/dist/components/SettingToggleUnit/index.d.ts +0 -3
  170. package/dist/components/SettingToggleUnit/index.d.ts.map +0 -1
  171. package/dist/components/SettingToggleUnit/types.d.ts +0 -11
  172. package/dist/components/SettingToggleUnit/types.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,34 +1,469 @@
1
1
  # @aleph-alpha/settings-kit
2
2
 
3
- Settings kit package delivering **setting units** and **generators** for Vue applications built on the Aleph Alpha Design System.
3
+ Turns two backend payloads into an accessible admin screen for Vue 3 applications built on the Aleph Alpha Design System:
4
4
 
5
- - **Setting units** — self-contained, typed descriptions of a single user-facing setting (id, label, description, default value) plus ready-made components that render them (e.g. `SettingToggleUnit`).
6
- - **Generators** — helpers such as `defineSettingUnit` and `createToggleUnit` that create typed setting units with sensible defaults.
5
+ - **Fields** (`SettingField[]`) say _what_ each setting is: type, value, constraints, rules, fallback.
6
+ - **Layout** (`SettingsLayout`) says _where_ each setting renders: category › page › section › group › item.
7
7
 
8
- The kit consumes the **published** `@aleph-alpha/ui-library` from npm (not the local workspace copy), so it always builds against the same version consumers install.
9
-
10
- ## Installation
8
+ Both are plain JSON. The kit renders them, stages edits in the state manager, validates, and saves through your `onSave`.
11
9
 
12
10
  ```bash
13
- pnpm add @aleph-alpha/settings-kit
11
+ pnpm add @aleph-alpha/settings-kit @aleph-alpha/ui-library vue
12
+ pnpm add vue-router # only for '@aleph-alpha/settings-kit/shell'
14
13
  ```
15
14
 
16
- ## Usage
15
+ ## Contents
17
16
 
18
- ```ts
19
- import { createToggleUnit, SettingToggleUnit } from '@aleph-alpha/settings-kit';
17
+ - [Quick start](#quick-start)
18
+ - [Layers](#layers)
19
+ - [The schema](#the-schema)
20
+ - [Field](#field)
21
+ - [Layout: category, page, section, group, item](#layout-category-page-section-group-item)
22
+ - [Actions](#actions)
23
+ - [Fallback](#fallback)
24
+ - [Rules](#rules)
25
+ - [Identity, ordering and validation](#identity-ordering-and-validation)
26
+ - [State manager integration](#state-manager-integration)
27
+ - [Actions at runtime](#actions-at-runtime)
28
+ - [Routing (SkShell)](#routing-skshell)
29
+ - [Customisation](#customisation)
30
+ - [Accessibility](#accessibility)
31
+ - [Styles](#styles)
32
+ - [Entry points](#entry-points)
33
+ - [Migration](#migration)
34
+ - [Development](#development)
35
+
36
+ More: [Overview](./docs/overview.md) · [Quick start](./docs/quick-start.md) · [Components](./docs/components.md) · [Generators](./docs/generators.md) · [Reference](./docs/reference.md)
37
+
38
+ ## Quick start
20
39
 
21
- const notifications = createToggleUnit({
22
- id: 'notifications',
23
- label: 'Enable notifications',
24
- description: 'Receive updates about your account',
40
+ ```vue
41
+ <script setup lang="ts">
42
+ import { useSettingsForm } from '@aleph-alpha/settings-kit';
43
+ import type { SettingField, SettingsLayout } from '@aleph-alpha/settings-kit';
44
+ import { SkShell } from '@aleph-alpha/settings-kit/shell';
45
+
46
+ const props = defineProps<{ fields: SettingField[]; layout: SettingsLayout }>();
47
+
48
+ const session = useSettingsForm({
49
+ fields: () => props.fields,
50
+ async onSave(changes) {
51
+ await api.saveSettings(changes); // { 'branding.appName': 'Phoenix', … }
52
+ },
25
53
  });
54
+ </script>
55
+
56
+ <template>
57
+ <SkShell :session="session" :layout="layout" base="/settings" back-href="/" />
58
+ </template>
59
+ ```
60
+
61
+ That renders the sidebar navigation, one page per URL (`/settings/<category>/<page>`), every field with its validation, and the page's Discard / Apply buttons, which call `session.discard()` and `session.save()`.
62
+
63
+ Without a router or chrome, render one page with `SkLayout`:
64
+
65
+ ```vue
66
+ <SkLayout v-model:page="pageId" :session="session" :layout="layout" />
67
+ ```
68
+
69
+ ## Layers
70
+
71
+ Each layer only depends on the ones below it, so you can stop at any level and build the rest yourself.
72
+
73
+ ```mermaid
74
+ flowchart TD
75
+ shell["Shell — SkShell, useShellLocation<br/>chrome, navigation, URLs (vue-router peer)"]
76
+ components["Components — SkLayout, SkPage/SkSection/SkGroup/SkItem, SkField<br/>default markup, accessible by default"]
77
+ composables["Composables — useSettingsForm, useSettingsLayout, useSettingField, useFieldValidator<br/>reactive state, no markup"]
78
+ core["Core — resolveLayout, validateLayout, toNavigation, rules, constraints, control registry<br/>plain TypeScript, no Vue reactivity"]
79
+ shell --> components --> composables --> core
80
+ ```
81
+
82
+ | Layer | Use it when | Main exports |
83
+ | ----------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
84
+ | Core | You need the data model only: a server check, a custom renderer, tests. | `resolveLayout`, `validateLayout`, `toNavigation`, `evaluateFieldRules`, `checkFieldConstraints`, `provideControls` |
85
+ | Composables | You render your own markup but want the kit's state, rules and validation. | `useSettingsForm`, `createStateManager`, `useSettingsLayout`, `useSettingField`, `useFieldValidator` |
86
+ | Components | You want the default page markup inside your own frame. | `SkLayout`, `SkField`, `SkPage`, `SkSection`, `SkGroup`, `SkItem` |
87
+ | Shell | You want the whole settings screen: top bar, sidebar navigation and one URL per page. | `SkShell` from `@aleph-alpha/settings-kit/shell` |
88
+
89
+ **Core** is pure: `resolveLayout(layout, { fieldByKey, valueOf })` turns the layout JSON into a `ResolvedLayout` tree — ordered, filtered by rules, with labels resolved for a locale and ids assigned. Everything above renders that tree; nothing re-implements ordering or rules.
90
+
91
+ **Composables** wrap the core in reactivity. `useSettingsLayout` is the heart of it: it connects a layout to a state manager and returns the resolved pages, the navigation, layout issues and validation errors. See [State manager integration](#state-manager-integration).
92
+
93
+ **Components** render a resolved page. `SkLayout` = `useSettingsLayout` + the page renderer. The four blocks (`SkPage`, `SkSection`, `SkGroup`, `SkItem`) are presentational and replaceable; `SkField` renders one setting with its control.
94
+
95
+ **Shell** adds the frame and routing. It is a separate entry point because it is the only part that touches `vue-router`, which is an optional peer.
96
+
97
+ ## The schema
98
+
99
+ ### Field
100
+
101
+ A `SettingField` is one setting. The layout only refers to it by `key`.
102
+
103
+ ```json
104
+ {
105
+ "key": "branding.appName",
106
+ "type": "string",
107
+ "label": { "en": "Application name" },
108
+ "description": { "en": "Shown in the browser tab." },
109
+ "value": "Phoenix chat",
110
+ "default": "",
111
+ "constraints": { "required": true, "maxLength": 30 },
112
+ "presentation": { "control": "text", "charCounter": true },
113
+ "rules": [],
114
+ "errors": { "required": { "en": "Give the application a name." } }
115
+ }
116
+ ```
117
+
118
+ | Property | Meaning |
119
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `key` | Unique id of the setting. Dots are just characters (`branding.appName`), not nesting. |
121
+ | `type` | Stored value type: `string`, `number`, `boolean`, `asset` (`asset://…` or `null`), or `actions` (no value — see [Actions](#actions)). |
122
+ | `label` | Localised name; required for every type except `actions`, so every input has an accessible name. |
123
+ | `value` | Stored value from the server. Edits are staged separately until saved. |
124
+ | `default` | Value used when `value` is absent. |
125
+ | `editable` | `false` disables the control. |
126
+ | `constraints` | Client validation: `required`, `maxLength`, `pattern`, `format`, `enum`, `mimeTypes`. |
127
+ | `errors` | Localised message per constraint name, overriding the defaults. |
128
+ | `presentation` | How it renders: `control`, `placeholder`, `charCounter`, `options` (select and actions), `inheritLabel`/`inheritHint` (fallback toggle). |
129
+ | `fallback` | Inherit another setting's value while a toggle is on. See [Fallback](#fallback). |
130
+ | `rules` | Show/hide and enable/disable from other settings' values. See [Rules](#rules). |
131
+
132
+ `presentation.control` picks the control: `text`, `select`, `checkbox`, `color`, `phone`, `period`, `image-upload`, `actions`, or any name you register with [`provideControls`](#controls). Without one, the type decides (`string` → text, `boolean` → checkbox, `asset` → image upload, `actions` → actions).
133
+
134
+ `LocalizedText` is `Record<locale, string>`. Call `provideSettingsLocale({ locale, fallbackLocale })` to choose the language. The kit uses the first non-empty text in this order: `locale`, `fallbackLocale`, `en`, any translation. See [Labels](#labels).
135
+
136
+ ### Layout: category, page, section, group, item
137
+
138
+ ```json
139
+ {
140
+ "categories": [
141
+ {
142
+ "uid": "appearance",
143
+ "label": { "en": "Appearance" },
144
+ "order": 1,
145
+ "pages": [
146
+ {
147
+ "uid": "branding",
148
+ "label": { "en": "Branding" },
149
+ "description": { "en": "How the application looks for your team." },
150
+ "order": 1,
151
+ "presentation": { "sidebar": true, "sidebarMode": "default" },
152
+ "sections": [
153
+ {
154
+ "uid": "general",
155
+ "order": 1,
156
+ "presentation": { "showHeader": false },
157
+ "items": [{ "key": "branding.appName" }]
158
+ },
159
+ {
160
+ "uid": "visual-style",
161
+ "label": { "en": "Visual style" },
162
+ "order": 2,
163
+ "items": [{ "key": "branding.actions" }],
164
+ "groups": [
165
+ {
166
+ "uid": "colors",
167
+ "label": { "en": "Colors" },
168
+ "order": 1,
169
+ "layout": { "columns": 2 },
170
+ "items": [
171
+ { "key": "branding.brandColor" },
172
+ { "key": "branding.accentColor", "column": 2 }
173
+ ]
174
+ }
175
+ ]
176
+ }
177
+ ]
178
+ }
179
+ ]
180
+ }
181
+ ]
182
+ }
183
+ ```
184
+
185
+ | Level | What it is | Renders as | Properties |
186
+ | ------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
187
+ | **Category** | A navigation group of pages. Never a screen of its own. | A labelled group in the sidebar navigation. | `uid`, `label`, `order`, `pages` |
188
+ | **Page** | One screen and one URL (`/<category>/<page>`). Also the save boundary for its actions. | `SkPage`: a `<form>` with heading, sections and the actions footer. | `uid`, `label`, `description?`, `order`, `presentation?` (`showHeader`, `sidebar`, `sidebarMode`: `default` 8/4, `wide` 7/5, `max` 6/6), `sections` |
189
+ | **Section** | A titled block of a page. Holds items directly, groups, or both. | `SkSection`: heading + grid. | `uid`, `label?`, `description?`, `order`, `presentation?.showHeader`, `layout?.columns`, `items?`, `groups?` |
190
+ | **Group** | A labelled cluster inside a section, with its own grid. | `SkGroup`: sub-heading + grid. | `uid`, `label?`, `description?`, `order`, `presentation?.showHeader`, `layout?.columns`, `items` |
191
+ | **Item** | A placement of one field. The only level that points at a field. | `SkItem`: one grid cell around `SkField`. | `key`, `order?`, `column?` (1-based pin) |
192
+
193
+ - **Labels are optional below the page.** A section or group without a `label`, or with `showHeader: false`, renders no heading. In the navigation, an unlabelled section or group hands its children to its parent.
194
+ - **Grids.** `layout.columns` (default 1) applies from the small breakpoint up; below it every grid is one column, in DOM order. `column` pins an item to a column at that breakpoint; pins outside `1…columns` are dropped.
195
+ - **Sidebar.** `presentation.sidebar` reserves the page's content sidebar; the host fills it through the `sidebar` slot of `SkShell`.
196
+
197
+ ### Actions
198
+
199
+ The page buttons are a field too, so the backend decides which buttons exist:
200
+
201
+ ```json
202
+ {
203
+ "key": "branding.actions",
204
+ "type": "actions",
205
+ "presentation": {
206
+ "options": [
207
+ { "value": "discard", "label": { "en": "Discard" } },
208
+ { "value": "apply", "label": { "en": "Apply changes" } }
209
+ ]
210
+ }
211
+ }
212
+ ```
213
+
214
+ - Place it like any item. Wherever it is placed on a page, it renders **once, in that page's footer**, not inline. A page without one has no buttons.
215
+ - Each option is a button; its `value` is the **method** sent with the [action event](#actions-at-runtime). `discard` and `apply` have built-in behaviour; any other name (`export`, `reset-defaults`, …) is yours to handle.
216
+ - The last button is the form's submit button, so <kbd>Enter</kbd> in a text field triggers it.
217
+ - `rules` work as on any field: hidden actions don't render, disabled ones render disabled. All buttons are also disabled while saving.
218
+
219
+ ### Fallback
220
+
221
+ A field can inherit another setting's value while a toggle is on — for example, the favicon reuses the logo:
222
+
223
+ ```json
224
+ {
225
+ "key": "branding.favicon",
226
+ "type": "asset",
227
+ "label": { "en": "Favicon" },
228
+ "value": null,
229
+ "fallback": { "key": "branding.logo" },
230
+ "presentation": { "inheritLabel": { "en": "Same as logo" } }
231
+ }
232
+ ```
233
+
234
+ - `SkField` renders the toggle (labelled `inheritLabel`) above the control. While it is on, the field shows and saves the source's value; turning it off restores the field's own value.
235
+ - On an `asset` field, a stored `null` means "inherit", so the toggle starts on.
236
+ - Fallbacks chain (`sales.phone` → `support.phone` → `org.phone`). `session.field(key).inheritedFrom` returns the final source.
237
+ - The toggle state lives in the state manager, so it survives page changes and is reset by `discard`.
238
+
239
+ ### Rules
240
+
241
+ A rule lives on the field it governs and names the controlling setting(s) inside `when`:
242
+
243
+ ```json
244
+ {
245
+ "key": "accessibility.motionLevel",
246
+ "type": "string",
247
+ "label": { "en": "Motion level" },
248
+ "rules": [
249
+ {
250
+ "effect": "SHOW",
251
+ "when": { "key": "accessibility.reduceMotion", "equals": true }
252
+ }
253
+ ]
254
+ }
255
+ ```
256
+
257
+ | Effect | Meaning |
258
+ | --------- | ----------------------------------- |
259
+ | `SHOW` | Hidden until the condition holds. |
260
+ | `HIDE` | Shown until the condition holds. |
261
+ | `ENABLE` | Disabled until the condition holds. |
262
+ | `DISABLE` | Enabled until the condition holds. |
263
+
264
+ - **Conditions** compare current, staged values: `equals`, `notEquals`, `in`, `notIn`, `gt`, `gte`, `lt`, `lte`, `isEmpty`, `notEmpty` (several on one leaf are ANDed), and combine with `{ allOf: [] }`, `{ anyOf: [] }`, `{ not: {} }`. Pure data — nothing is evaluated as code.
265
+ - **Several rules all apply**: any hiding rule wins over showing, any disabling rule over enabling.
266
+ - **In the layout**, hidden items are removed from the resolved tree, and a group or section left empty disappears with them. Navigation and validation only see what is visible, so a hidden required field never blocks saving.
267
+ - `validateFieldRules(fields)` throws on a rule naming an unknown key, and on cycles across rules and fallbacks (A gates B gates A).
268
+
269
+ ### Identity, ordering and validation
270
+
271
+ - **Ids.** Every resolved node has an `id`: the URI-encoded uid path, e.g. `appearance/branding/colors`. A page id (`appearance/branding`) is what `v-model:page` takes and what the URL shows.
272
+ - **Uids** must be non-empty and unique among siblings. If two siblings share one, the first wins and the rest are skipped.
273
+ - **Keys** may be placed once in the whole layout. Unknown keys and repeated placements are skipped.
274
+ - **Order.** Siblings sort by `order` ascending; ties keep array order. Items without `order` keep their array position and come before the section's groups; give items an `order` to interleave them with groups.
275
+
276
+ `validateLayout(layout, fields)` reports every one of these as a `LayoutIssue` (`{ code, severity, path, message }`) instead of skipping silently. `useSettingsLayout(...).issues` runs it for you — log it in development or reject the payload server-side.
277
+
278
+ | Code | Severity | Meaning |
279
+ | ---------------- | -------- | ------------------------------------------------------------------------------- |
280
+ | `empty-uid` | error | A category, page, section or group has an empty `uid`. |
281
+ | `duplicate-uid` | error | Two siblings share a `uid`; only the first renders. |
282
+ | `unknown-key` | error | An item names a key no field has. |
283
+ | `duplicate-key` | error | A key is placed twice; only the first renders. |
284
+ | `invalid-column` | warning | `column` is outside the parent's `columns`; the pin is dropped. |
285
+ | `path-collision` | warning | Two page URLs differ only in letter case, so they can't be told apart in a URL. |
286
+ | `unplaced-field` | warning | A field is not placed anywhere, so it can never be edited. |
287
+
288
+ ## State manager integration
289
+
290
+ The layout never owns values. Every field the layout shows reads from and stages into one **state manager** (`createStateManager`), the same one `useSettingsForm` creates and provides.
291
+
292
+ ```mermaid
293
+ flowchart LR
294
+ api[(Backend)] -- fields --> sm["State manager<br/>useSettingsForm"]
295
+ api -- layout --> sl["useSettingsLayout"]
296
+ sm -- fieldByKey / valueOf --> sl
297
+ sl -- resolved page --> ui["SkLayout / SkShell"]
298
+ ui -- edits --> sm
299
+ ui -- "action: apply" --> sm
300
+ sm -- "onSave(changes)" --> api
301
+ ```
302
+
303
+ `SkLayout`, `SkShell` and `useSettingsLayout` pick the state manager in this order:
304
+
305
+ 1. the `session` prop;
306
+ 2. the nearest `useSettingsForm` up the component tree;
307
+ 3. a private one built from the `fields` prop (a quick demo; nothing is saved).
308
+
309
+ Fields passed through `fields` that the state manager doesn't know yet are registered into it, so a layout can bring its own fields into a shared form.
310
+
311
+ Because the state lives in the state manager, not in components:
312
+
313
+ - leaving a page and coming back keeps unsaved edits;
314
+ - `session.isDirty`, `session.save()`, `session.discard()` and `session.field(key)` all reflect what the layout shows;
315
+ - several layouts (or a layout plus your own `useSettingField` forms) can share one save.
316
+
317
+ Validation runs over the placed, visible, non-action fields of the layout. Client errors appear once a field is edited, or for every field after a failed Apply. Server issues (`model.validator.applyServerIssues(...)`) show immediately.
318
+
319
+ To render your own markup with the same behaviour, use the composable directly:
320
+
321
+ ```ts
322
+ const model = useSettingsLayout({ layout: () => props.layout, session });
323
+
324
+ model.pages.value; // ResolvedPage[] in display order
325
+ model.page('appearance/branding');
326
+ model.navigation.value; // LayoutNavigationNode[] with `invalid` flags
327
+ model.issues.value; // LayoutIssue[]
328
+ model.errorsByKey.value; // Record<key, string[]> to show right now
329
+ model.revealErrors(); // show client errors for untouched fields too
330
+ ```
331
+
332
+ `useSettingsLayout` reads the locale from `provideSettingsLocale` in a parent component. If the same component provides the locale, pass it: `useSettingsLayout({ layout, session, settingsLocale })`, where `settingsLocale` is the value that `provideSettingsLocale` returns.
333
+
334
+ ## Actions at runtime
335
+
336
+ Every button press emits one `action` event from `SkLayout` / `SkShell`:
337
+
338
+ ```ts
339
+ interface SkLayoutActionEvent {
340
+ method: string; // the option value, e.g. 'apply'
341
+ key: string; // the actions field, e.g. 'branding.actions'
342
+ pageId: string; // e.g. 'appearance/branding'
343
+ preventDefault(): void;
344
+ readonly defaultPrevented: boolean;
345
+ }
346
+ ```
347
+
348
+ Your listener runs first. Unless it calls `preventDefault()`, the built-in behaviour follows:
349
+
350
+ | Method | Built-in behaviour |
351
+ | --------- | ------------------------------------------------------------------------------------------------- |
352
+ | `discard` | `session.discard()`, clear server issues, hide revealed errors. |
353
+ | `apply` | Validate. If invalid, reveal every error. Otherwise `session.save()` (your `onSave`), then reset. |
354
+ | other | Nothing — handle it in your listener. |
355
+
356
+ A failed save emits `error` with what `onSave` threw.
357
+
358
+ ```vue
359
+ <SkShell
360
+ :session="session"
361
+ :layout="layout"
362
+ @action="onAction"
363
+ @error="toast.error"
364
+ />
26
365
  ```
27
366
 
367
+ ```ts
368
+ function onAction(event: SkLayoutActionEvent) {
369
+ if (event.method === 'apply' && !window.confirm('Publish to all users?')) {
370
+ event.preventDefault();
371
+ }
372
+ if (event.method === 'export') downloadSettings(event.pageId);
373
+ }
374
+ ```
375
+
376
+ ## Routing (SkShell)
377
+
378
+ `SkShell` publishes each page at `${base}/<category>/<page>`. Paths are always built from the uids; the `base` prop (default `''`) is the prefix.
379
+
380
+ - **With a host `vue-router`**, render `SkShell` in a route that covers its sub-paths (`{ path: '/settings/:page(.*)*', component: SettingsView }`). The shell navigates with your router, only while its own route is active, and returns to the actual location if a guard cancels navigation.
381
+ - **Without a router**, it uses the browser history directly, and never rewrites URLs outside `base`.
382
+ - Links are real `<a href>`s, so open-in-new-tab and copy-link work; plain clicks navigate in place.
383
+ - URL matching ignores letter case and a trailing slash, and a non-canonical URL is replaced by the canonical one, keeping `?query` and `#hash`.
384
+ - An unknown path inside `base` falls back to the first page.
385
+ - `v-model:page` reads and sets the page id, two-way with the URL.
386
+ - Inside `<KeepAlive>`, a deactivated shell stops following the URL and catches up when reactivated.
387
+
388
+ ## Customisation
389
+
390
+ ### Blocks
391
+
392
+ Replace the component used at any level; the rest keep their defaults:
393
+
28
394
  ```vue
29
- <SettingToggleUnit v-model="enabled" :unit="notifications" />
395
+ <SkLayout :layout="layout" :blocks="{ section: MyCard, item: MyItem }" />
396
+ ```
397
+
398
+ A block receives the same props as the default (`SkPage`, `SkSection`, `SkGroup`, `SkItem`) and renders its default slot. Keep the `level` prop on your heading so the outline stays sequential.
399
+
400
+ ### Controls
401
+
402
+ Register a control for a `presentation.control` name. Registrations merge over the parent's, so you can override a built-in or add your own:
403
+
404
+ ```ts
405
+ import { provideControls } from '@aleph-alpha/settings-kit';
406
+
407
+ provideControls({
408
+ slider: {
409
+ component: MySlider,
410
+ kind: 'input', // edits a value; 'button' emits `action` instead
411
+ props: (field, { label }) => ({ label, max: field.presentation?.max }),
412
+ },
413
+ });
414
+ ```
415
+
416
+ A field with `presentation: { control: 'slider' }` now renders `MySlider` with `v-model` wired by `SkField`; `props` maps the schema to the component's props. Unknown names render a visible "unsupported control" placeholder.
417
+
418
+ ### Labels
419
+
420
+ Call `provideSettingsLocale({ locale, fallbackLocale, messages })` once, in a parent of the shell. Schema text and the kit wording (`en` and `de`) follow it. The chrome text of `SkShell` (title, back, navigation, "has errors", …) uses the `shell.*` keys of the kit wording. Set `labels` to replace single entries. `labels` wins over `messages`. `defaultShellLabels` holds the `en` text.
421
+
422
+ ## Accessibility
423
+
424
+ - Heading levels follow the structure: page = `headingLevel` (default 1), sections one below, groups one below a titled section. Set `headingLevel` when the shell sits under an existing `h1`.
425
+ - Each page is a `<form novalidate>`; errors are announced per field and linked with `aria-errormessage`.
426
+ - The navigation marks the current page with `aria-current="page"`, and pages with errors get a visible dot plus a screen-reader text ("has errors").
427
+ - Grids reflow to one column at narrow widths and 400% zoom, in DOM order, so the reading order never depends on the column pin.
428
+ - Without `backHref` and without a `back` listener, no back control is rendered.
429
+
430
+ ## Styles
431
+
432
+ Components use UnoCSS classes from the Aleph Alpha preset; the kit ships no CSS file. Add the kit's `dist` to your UnoCSS content so its classes are generated, as you already do for the ui-library:
433
+
434
+ ```ts
435
+ // uno.config.ts
436
+ content: {
437
+ filesystem: [
438
+ './node_modules/@aleph-alpha/ui-library/dist/**/*.js',
439
+ './node_modules/@aleph-alpha/settings-kit/dist/**/*.js',
440
+ ],
441
+ },
30
442
  ```
31
443
 
444
+ ## Entry points
445
+
446
+ | Import | Contents | Peers |
447
+ | -------------------------------------- | --------------------------------------------------------- | -------------------------------- |
448
+ | `@aleph-alpha/settings-kit` | Core, composables, components and all types. | `vue`, `@aleph-alpha/ui-library` |
449
+ | `@aleph-alpha/settings-kit/components` | Components only. | same |
450
+ | `@aleph-alpha/settings-kit/shell` | `SkShell`, `SkShellFrame`, `SkShellNav`, routing helpers. | + `vue-router` (optional peer) |
451
+ | `@aleph-alpha/settings-kit/generators` | `defineSettingUnit`, `createToggleUnit` (alias `/units`). | none |
452
+
453
+ ## Migration
454
+
455
+ From the earlier preview of the layout builder:
456
+
457
+ | Before | Now |
458
+ | ---------------------------------------------- | ------------------------------------------------------------------------------- |
459
+ | `SkCategory`, `SkCategoryNav`, `SkFormActions` | `SkLayout` (one page) or `SkShell` (screen with navigation). |
460
+ | `useBlockGenerator` | `useSettingsLayout`, or `resolveLayout` in plain TypeScript. |
461
+ | `@discard` / `@apply` events | One `@action` event with `method`; built-in handling unless `preventDefault()`. |
462
+ | `fields` prop holding the state | Values live in the state manager (`session` prop or `useSettingsForm`). |
463
+ | URLs by label, `SkShell` from the root entry | `/<category>/<page>` from uids; import `SkShell` from `/shell`. |
464
+ | `valueFrom` on rules, `faviconSameAsLogo` | `fallback: { key }` on the inheriting field. |
465
+ | `page.label.en` in slots | `page.label` (already resolved). |
466
+
32
467
  ## Development
33
468
 
34
469
  ```bash
@@ -37,4 +472,19 @@ npx nx run settings-kit:test # Unit tests (Vitest)
37
472
  npx nx run settings-kit:lint # Prettier + ESLint
38
473
  npx nx run settings-kit:build # Library build + type declarations
39
474
  npx nx run settings-kit:test-storybook # Storybook interaction & a11y tests
475
+ pnpm --filter @aleph-alpha/settings-kit playground # Live demo against the source
40
476
  ```
477
+
478
+ The kit builds against the **published** `@aleph-alpha/ui-library` from npm, not the workspace copy, so it always matches what consumers install.
479
+
480
+ ## Releasing
481
+
482
+ Releases are automated with [Release Please](https://github.com/googleapis/release-please), like every other package in this repo:
483
+
484
+ 1. Merge a PR to `main` with a conventional commit scoped to the kit (`feat(settings-kit): …`, `fix(settings-kit): …`). Only commits touching `packages/settings-kit/**` count.
485
+ 2. Release Please opens or updates the `chore(release): settings-kit <version>` PR with the version bump and `CHANGELOG.md` entry.
486
+ 3. Merging that PR tags `settings-kit-<version>` and the `Release` workflow publishes `@aleph-alpha/settings-kit` to npm.
487
+
488
+ While the kit is below `1.0.0`, a breaking change (`feat!:` or a `BREAKING CHANGE:` footer) bumps the major version to `1.0.0`.
489
+
490
+ See [GitHub CI Flow](../../.github/documentation/GITHUB_CI_FLOW.md#release-flow) for the full pipeline and troubleshooting.
@@ -0,0 +1,51 @@
1
+ import { defineComponent, useModel, computed, openBlock, createBlock, unref, renderSlot, mergeModels } from "vue";
2
+ import { H as useSettingsLayout, r as provideSkLayoutContext, S as SkLayoutPage } from "./SkLayoutPage-DGbCpQFm.js";
3
+ const _sfc_main = /* @__PURE__ */ defineComponent({
4
+ ...{
5
+ name: "SkLayout"
6
+ },
7
+ __name: "SkLayout",
8
+ props: /* @__PURE__ */ mergeModels({
9
+ layout: {},
10
+ session: { default: void 0 },
11
+ fields: { default: void 0 },
12
+ validators: { default: void 0 },
13
+ blocks: { default: void 0 },
14
+ headingLevel: { default: 1 }
15
+ }, {
16
+ "page": {},
17
+ "pageModifiers": {}
18
+ }),
19
+ emits: /* @__PURE__ */ mergeModels(["action", "error"], ["update:page"]),
20
+ setup(__props, { expose: __expose, emit: __emit }) {
21
+ const props = __props;
22
+ const page = useModel(__props, "page");
23
+ const emit = __emit;
24
+ const model = useSettingsLayout({
25
+ layout: () => props.layout,
26
+ session: props.session,
27
+ fields: () => props.fields,
28
+ validators: props.validators
29
+ });
30
+ provideSkLayoutContext(model, {
31
+ blocks: () => props.blocks,
32
+ headingLevel: () => props.headingLevel,
33
+ onAction: (event) => emit("action", event),
34
+ onError: (error) => emit("error", error)
35
+ });
36
+ const current = computed(() => model.page(page.value) ?? model.pages.value[0]);
37
+ __expose({
38
+ /** The layout model: session, resolved tree, navigation, validator. */
39
+ model
40
+ });
41
+ return (_ctx, _cache) => {
42
+ return current.value ? (openBlock(), createBlock(unref(SkLayoutPage), {
43
+ key: current.value.id,
44
+ page: current.value
45
+ }, null, 8, ["page"])) : renderSlot(_ctx.$slots, "empty", { key: 1 });
46
+ };
47
+ }
48
+ });
49
+ export {
50
+ _sfc_main as _
51
+ };