qdadm 0.13.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 (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. package/src/utils/transformers.js +105 -0
@@ -0,0 +1,165 @@
1
+ import { computed, inject } from 'vue'
2
+
3
+ /**
4
+ * Composable for building sub-editor components that edit a portion of parent form data.
5
+ *
6
+ * Sub-editors receive data via v-model and emit updates, but don't handle persistence.
7
+ * The parent form (using useForm/useBareForm) handles loading, saving, and dirty tracking.
8
+ *
9
+ * Usage in sub-editor:
10
+ * const props = defineProps({ modelValue: Object })
11
+ * const emit = defineEmits(['update:modelValue'])
12
+ *
13
+ * const { data, update, field } = useSubEditor(props, emit)
14
+ *
15
+ * // Access data
16
+ * data.value.someField
17
+ *
18
+ * // Update single field
19
+ * update('someField', newValue)
20
+ *
21
+ * // Computed getter/setter for field
22
+ * const myField = field('someField', defaultValue)
23
+ * myField.value = 'new value' // auto-emits update
24
+ *
25
+ * // Nested field access
26
+ * const nested = field('config.subsection.value', 0)
27
+ *
28
+ * Features:
29
+ * - Simplified update pattern (no manual spread)
30
+ * - Computed fields with get/set for v-model binding
31
+ * - Nested path support (dot notation)
32
+ * - Default values
33
+ * - Optional dirty field indicator integration with parent
34
+ *
35
+ * @param {Object} props - Component props (must include modelValue)
36
+ * @param {Function} emit - Component emit function
37
+ * @param {Object} options - Additional options
38
+ * @param {*} options.defaultData - Default data structure if modelValue is empty
39
+ */
40
+ export function useSubEditor(props, emit, options = {}) {
41
+ const { defaultData = {} } = options
42
+
43
+ // Computed reference to data (with default fallback)
44
+ const data = computed(() => props.modelValue ?? defaultData)
45
+
46
+ /**
47
+ * Get value at nested path
48
+ */
49
+ function getNestedValue(obj, path) {
50
+ if (!path.includes('.')) {
51
+ return obj?.[path]
52
+ }
53
+ return path.split('.').reduce((curr, key) => curr?.[key], obj)
54
+ }
55
+
56
+ /**
57
+ * Set value at nested path, returning new object
58
+ */
59
+ function setNestedValue(obj, path, value) {
60
+ if (!path.includes('.')) {
61
+ return { ...obj, [path]: value }
62
+ }
63
+
64
+ const keys = path.split('.')
65
+ const result = { ...obj }
66
+ let current = result
67
+
68
+ for (let i = 0; i < keys.length - 1; i++) {
69
+ const key = keys[i]
70
+ current[key] = { ...current[key] }
71
+ current = current[key]
72
+ }
73
+
74
+ current[keys[keys.length - 1]] = value
75
+ return result
76
+ }
77
+
78
+ /**
79
+ * Update a field and emit the new value
80
+ * @param {string} path - Field path (supports dot notation for nested)
81
+ * @param {*} value - New value
82
+ */
83
+ function update(path, value) {
84
+ const newData = setNestedValue(data.value, path, value)
85
+ emit('update:modelValue', newData)
86
+ }
87
+
88
+ /**
89
+ * Replace entire data object
90
+ * @param {Object} newData - Complete new data object
91
+ */
92
+ function replace(newData) {
93
+ emit('update:modelValue', newData)
94
+ }
95
+
96
+ /**
97
+ * Create a computed ref for a field with get/set
98
+ * Useful for v-model binding in template
99
+ *
100
+ * @param {string} path - Field path (supports dot notation)
101
+ * @param {*} defaultValue - Default value if field is undefined
102
+ * @returns {ComputedRef} Writable computed ref
103
+ */
104
+ function field(path, defaultValue = undefined) {
105
+ return computed({
106
+ get: () => {
107
+ const value = getNestedValue(data.value, path)
108
+ return value !== undefined ? value : defaultValue
109
+ },
110
+ set: (value) => update(path, value)
111
+ })
112
+ }
113
+
114
+ /**
115
+ * Create multiple field refs at once
116
+ * @param {Object} fields - { fieldName: defaultValue, ... }
117
+ * @returns {Object} { fieldName: computedRef, ... }
118
+ */
119
+ function fields(fieldDefs) {
120
+ const result = {}
121
+ for (const [path, defaultValue] of Object.entries(fieldDefs)) {
122
+ result[path] = field(path, defaultValue)
123
+ }
124
+ return result
125
+ }
126
+
127
+ /**
128
+ * Get value with default fallback (non-reactive, for one-time reads)
129
+ */
130
+ function get(path, defaultValue = undefined) {
131
+ const value = getNestedValue(data.value, path)
132
+ return value !== undefined ? value : defaultValue
133
+ }
134
+
135
+ // Try to inject field-level dirty tracking from parent (if available)
136
+ const parentIsFieldDirty = inject('isFieldDirty', null)
137
+
138
+ /**
139
+ * Check if a field is dirty (if parent provides tracking)
140
+ * This allows sub-editors to show field-level dirty indicators
141
+ */
142
+ function isFieldDirty(path) {
143
+ if (!parentIsFieldDirty) return false
144
+ // The parent tracks 'form.value.fieldName' or similar
145
+ // We need to prefix with the path the parent uses
146
+ return parentIsFieldDirty(path)
147
+ }
148
+
149
+ return {
150
+ // Data access
151
+ data,
152
+ get,
153
+
154
+ // Updates
155
+ update,
156
+ replace,
157
+
158
+ // Field helpers
159
+ field,
160
+ fields,
161
+
162
+ // Dirty tracking (from parent)
163
+ isFieldDirty
164
+ }
165
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * useTabSync - Composable for tab navigation with URL hash sync
3
+ *
4
+ * Provides:
5
+ * - activeTab computed from URL hash
6
+ * - onTabChange handler to update URL
7
+ * - Support for conditional tabs (edit mode only)
8
+ * - Lazy loading trigger on tab activation
9
+ *
10
+ * Usage:
11
+ * const { activeTab, onTabChange } = useTabSync({
12
+ * validTabs: ['general', 'style', 'behavior', 'newsrooms'],
13
+ * defaultTab: 'general',
14
+ * restrictedTabs: { newsrooms: () => isEdit.value },
15
+ * onTabActivate: { newsrooms: () => loadNewsroomData() }
16
+ * })
17
+ */
18
+
19
+ import { computed } from 'vue'
20
+ import { useRoute, useRouter } from 'vue-router'
21
+
22
+ /**
23
+ * @param {Object} options
24
+ * @param {string[]} options.validTabs - List of valid tab values
25
+ * @param {string} [options.defaultTab='general'] - Default tab when hash is invalid
26
+ * @param {Object.<string, Function>} [options.restrictedTabs={}] - Tab name -> condition function
27
+ * @param {Object.<string, Function>} [options.onTabActivate={}] - Tab name -> callback on first activation
28
+ */
29
+ export function useTabSync(options = {}) {
30
+ const {
31
+ validTabs = ['general'],
32
+ defaultTab = 'general',
33
+ restrictedTabs = {},
34
+ onTabActivate = {}
35
+ } = options
36
+
37
+ const route = useRoute()
38
+ const router = useRouter()
39
+
40
+ // Track which tabs have been activated (for lazy loading)
41
+ const activatedTabs = new Set([defaultTab])
42
+
43
+ /**
44
+ * Active tab derived from URL hash
45
+ */
46
+ const activeTab = computed(() => {
47
+ const hash = route.hash?.replace('#', '')
48
+
49
+ if (hash && validTabs.includes(hash)) {
50
+ // Check if tab is restricted
51
+ const restriction = restrictedTabs[hash]
52
+ if (restriction && !restriction()) {
53
+ return defaultTab
54
+ }
55
+ return hash
56
+ }
57
+
58
+ return defaultTab
59
+ })
60
+
61
+ /**
62
+ * Handle tab change - update URL hash and trigger callbacks
63
+ * @param {string} newTab
64
+ */
65
+ function onTabChange(newTab) {
66
+ // Update URL hash
67
+ router.replace({ ...route, hash: `#${newTab}` })
68
+
69
+ // Trigger lazy load callback if first activation
70
+ if (!activatedTabs.has(newTab)) {
71
+ activatedTabs.add(newTab)
72
+ const callback = onTabActivate[newTab]
73
+ if (callback) {
74
+ callback()
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if a tab should be visible
81
+ * @param {string} tabName
82
+ * @returns {boolean}
83
+ */
84
+ function isTabVisible(tabName) {
85
+ const restriction = restrictedTabs[tabName]
86
+ if (restriction) {
87
+ return restriction()
88
+ }
89
+ return true
90
+ }
91
+
92
+ /**
93
+ * Navigate to a specific tab programmatically
94
+ * @param {string} tabName
95
+ */
96
+ function goToTab(tabName) {
97
+ if (validTabs.includes(tabName) && isTabVisible(tabName)) {
98
+ onTabChange(tabName)
99
+ }
100
+ }
101
+
102
+ return {
103
+ activeTab,
104
+ onTabChange,
105
+ isTabVisible,
106
+ goToTab
107
+ }
108
+ }
109
+
110
+ export default useTabSync
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Composable for guarding against unsaved changes when leaving a page
3
+ *
4
+ * Usage:
5
+ * const { dirty, takeSnapshot, checkDirty } = useDirtyState(...)
6
+ * useUnsavedChangesGuard(dirty, {
7
+ * onSave: () => handleSubmit(false),
8
+ * message: 'You have unsaved changes.'
9
+ * })
10
+ */
11
+
12
+ import { onBeforeUnmount, ref } from 'vue'
13
+ import { onBeforeRouteLeave } from 'vue-router'
14
+
15
+ /**
16
+ * Guard against leaving page with unsaved changes
17
+ *
18
+ * Shows a dialog with up to 3 options:
19
+ * - "Save & Leave" (if onSave provided) - saves then navigates
20
+ * - "Leave" - navigates without saving (discards changes)
21
+ * - "Stay" - cancels navigation
22
+ *
23
+ * @param {Ref<boolean>} dirty - Reactive dirty state
24
+ * @param {Object} options - Options
25
+ * @param {Function} options.onSave - Async function to save changes
26
+ * @param {string} options.message - Custom message (optional)
27
+ * @param {boolean} options.browserGuard - Show native browser dialog on tab close (default: false)
28
+ */
29
+ export function useUnsavedChangesGuard(dirty, options = {}) {
30
+ const {
31
+ onSave = null,
32
+ message = 'You have unsaved changes that will be lost.',
33
+ browserGuard = false // Disabled by default - use soft JS modal only
34
+ } = options
35
+
36
+ // State for custom dialog
37
+ const showDialog = ref(false)
38
+ const pendingNext = ref(null)
39
+ const saving = ref(false)
40
+
41
+ // Browser beforeunload event (for closing tab/window) - optional
42
+ function handleBeforeUnload(e) {
43
+ if (dirty.value) {
44
+ e.preventDefault()
45
+ e.returnValue = message
46
+ return message
47
+ }
48
+ }
49
+
50
+ // Add browser event listener only if browserGuard is enabled
51
+ if (browserGuard && typeof window !== 'undefined') {
52
+ window.addEventListener('beforeunload', handleBeforeUnload)
53
+ }
54
+
55
+ // Clean up on unmount
56
+ onBeforeUnmount(() => {
57
+ if (browserGuard && typeof window !== 'undefined') {
58
+ window.removeEventListener('beforeunload', handleBeforeUnload)
59
+ }
60
+ })
61
+
62
+ // Dialog actions
63
+ async function handleSaveAndLeave() {
64
+ if (!onSave || !pendingNext.value) return
65
+
66
+ saving.value = true
67
+ try {
68
+ await onSave()
69
+ showDialog.value = false
70
+ pendingNext.value()
71
+ pendingNext.value = null
72
+ } catch (e) {
73
+ console.error('Save failed:', e)
74
+ // Stay on page - don't close dialog, let user retry or choose Leave
75
+ } finally {
76
+ saving.value = false
77
+ }
78
+ }
79
+
80
+ function handleLeave() {
81
+ if (!pendingNext.value) return
82
+ showDialog.value = false
83
+ pendingNext.value()
84
+ pendingNext.value = null
85
+ }
86
+
87
+ function handleStay() {
88
+ showDialog.value = false
89
+ if (pendingNext.value) {
90
+ pendingNext.value(false)
91
+ pendingNext.value = null
92
+ }
93
+ }
94
+
95
+ // Vue Router navigation guard
96
+ onBeforeRouteLeave((to, from, next) => {
97
+ if (!dirty.value) {
98
+ next()
99
+ return
100
+ }
101
+
102
+ // Store the next callback and show dialog
103
+ pendingNext.value = next
104
+ showDialog.value = true
105
+ })
106
+
107
+ return {
108
+ dirty,
109
+ // Dialog state and handlers for custom dialog component
110
+ guardDialog: {
111
+ visible: showDialog,
112
+ saving,
113
+ message,
114
+ hasOnSave: !!onSave,
115
+ onSaveAndLeave: handleSaveAndLeave,
116
+ onLeave: handleLeave,
117
+ onStay: handleStay
118
+ }
119
+ }
120
+ }
121
+
122
+ export default useUnsavedChangesGuard