@getdom/studio 0.1.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 (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,128 @@
1
+ <script setup>
2
+ import { computed, inject } from 'vue';
3
+ import DomTooltip from '../../components/tooltip/DomTooltip.vue';
4
+ import { formFieldProviderKey } from '../field/useField.js';
5
+ import { passwordStrength } from './passwordStrength.js';
6
+
7
+ defineOptions({
8
+ __doc: {
9
+ name: 'Password strength',
10
+ tag: '<DomPasswordStrength>',
11
+ description: 'A display-only password strength meter that can read a named DomForm field or a direct model value.',
12
+ icon: 'M4 18h3V9H4v9Zm6 0h3V6h-3v12Zm6 0h3V3h-3v15Z',
13
+ playground: {
14
+ initial: {
15
+ modelValue: 'violet-orbit-lantern-47!',
16
+ },
17
+ },
18
+ },
19
+ });
20
+
21
+ const props = defineProps({
22
+ modelValue: {
23
+ type: String,
24
+ default: '',
25
+ _edit: { description: 'Password value to assess when no form field name is provided.' },
26
+ },
27
+ name: {
28
+ type: String,
29
+ default: '',
30
+ _edit: { description: 'Named DomForm field whose current value should be assessed.' },
31
+ },
32
+ showCrackTime: {
33
+ type: Boolean,
34
+ default: true,
35
+ _edit: { description: 'Show the estimated time required to crack the password.' },
36
+ },
37
+ descriptionMode: {
38
+ type: String,
39
+ default: 'inline',
40
+ _edit: {
41
+ options: ['inline', 'tooltip'],
42
+ description: 'Show the strength description below the meter or in a tooltip.',
43
+ },
44
+ },
45
+ tooltipPlacement: {
46
+ type: String,
47
+ default: 'bottom',
48
+ _edit: {
49
+ options: ['top', 'bottom', 'left', 'right'],
50
+ description: 'Position of the strength description when descriptionMode is tooltip.',
51
+ },
52
+ },
53
+ visible: {
54
+ type: Boolean,
55
+ default: true,
56
+ _edit: { description: 'Show or hide the strength meter.' },
57
+ },
58
+ });
59
+
60
+ const form = inject(formFieldProviderKey, null);
61
+ const strengthTones = ['', 'bg-destructive', 'bg-destructive', 'bg-warning', 'bg-warning', 'bg-success'];
62
+
63
+ /**
64
+ * Resolves the password from a named form field or the direct component value.
65
+ *
66
+ * @returns {string} Current password value.
67
+ */
68
+ function resolvedPassword() {
69
+ if (props.name && typeof form?.getValue === 'function') {
70
+ const formValue = form.getValue(props.name);
71
+ if (formValue !== undefined) return String(formValue ?? '');
72
+ }
73
+ return String(props.modelValue ?? '');
74
+ }
75
+
76
+ /**
77
+ * Builds the visible and accessible description for the current password strength.
78
+ *
79
+ * @returns {string} Password strength label with an optional crack-time estimate.
80
+ */
81
+ function strengthDescription() {
82
+ if (!password.value) return 'Password strength not yet available';
83
+
84
+ const crackTime = props.showCrackTime && passwordInfo.value.crackTime
85
+ ? ` (Time to crack: ${passwordInfo.value.crackTime})`
86
+ : '';
87
+
88
+ return `${passwordInfo.value.label} password${crackTime}`;
89
+ }
90
+
91
+ const password = computed(resolvedPassword);
92
+ const passwordInfo = computed(() => passwordStrength(password.value));
93
+ const description = computed(strengthDescription);
94
+ </script>
95
+
96
+ <template>
97
+ <div
98
+ v-if="visible"
99
+ class="dom-password-strength w-full"
100
+ :class="{ 'space-y-2': descriptionMode === 'inline' }"
101
+ role="status"
102
+ aria-live="polite"
103
+ :aria-label="description"
104
+ >
105
+ <component
106
+ :is="descriptionMode === 'tooltip' ? DomTooltip : 'div'"
107
+ v-bind="descriptionMode === 'tooltip' ? { text: description, placement: tooltipPlacement } : {}"
108
+ class="w-full"
109
+ >
110
+ <div
111
+ class="grid w-full grid-cols-5 gap-1.5"
112
+ :aria-hidden="descriptionMode === 'inline' ? 'true' : undefined"
113
+ :aria-label="descriptionMode === 'tooltip' ? description : undefined"
114
+ :tabindex="descriptionMode === 'tooltip' ? 0 : undefined"
115
+ >
116
+ <span
117
+ v-for="step in 5"
118
+ :key="step"
119
+ class="h-1.5 rounded-full transition"
120
+ :class="step <= passwordInfo.score ? strengthTones[passwordInfo.score] : 'bg-muted'"
121
+ ></span>
122
+ </div>
123
+ </component>
124
+ <p v-if="descriptionMode === 'inline'" class="min-h-4 text-xs text-muted-fg">
125
+ {{ password ? description : '' }}
126
+ </p>
127
+ </div>
128
+ </template>
@@ -0,0 +1,92 @@
1
+ const SECONDS_PER_YEAR = 31557600;
2
+
3
+ /**
4
+ * Formats an estimated cracking duration for human-readable feedback.
5
+ *
6
+ * @param {number} seconds Estimated cracking duration in seconds.
7
+ * @returns {string} Human-readable duration.
8
+ */
9
+ function secondsToHuman(seconds) {
10
+ if (seconds < 1) return 'instantly';
11
+ if (seconds < 60) return `${Math.round(seconds)} seconds`;
12
+ if (seconds < 3600) return `${Math.round(seconds / 60)} minutes`;
13
+ if (seconds < 86400) return `${Math.round(seconds / 3600)} hours`;
14
+ if (seconds < SECONDS_PER_YEAR) return `${Math.round(seconds / 86400)} days`;
15
+ const years = seconds / SECONDS_PER_YEAR;
16
+ if (years < 1_000) return `${Math.round(years)} years`;
17
+ if (years < 1_000_000) return `${Math.round(years / 1_000)} thousand years`;
18
+ if (years < 1_000_000_000) return `${Math.round(years / 1_000_000)} million years`;
19
+ if (years < 1_000_000_000_000) return `${Math.round(years / 1_000_000_000)} billion years`;
20
+ return 'trillions of years';
21
+ }
22
+
23
+ /**
24
+ * Converts a cracking duration into the five-step strength scale.
25
+ *
26
+ * @param {number} seconds Estimated cracking duration in seconds.
27
+ * @returns {[number, string]} Strength score and label.
28
+ */
29
+ function scoreFromCrackSeconds(seconds) {
30
+ if (seconds < SECONDS_PER_YEAR) return [1, 'Very weak'];
31
+ if (seconds < SECONDS_PER_YEAR * 1_000) return [2, 'Weak'];
32
+ if (seconds < SECONDS_PER_YEAR * 1_000_000) return [3, 'Okay'];
33
+ if (seconds < SECONDS_PER_YEAR * 1_000_000_000) return [4, 'Strong'];
34
+ return [5, 'Very strong'];
35
+ }
36
+
37
+ /**
38
+ * Estimates average offline cracking time from password entropy.
39
+ *
40
+ * @param {number} entropy Estimated password entropy in bits.
41
+ * @param {number} [guessesPerSecond=1000000000000] Assumed cracking rate.
42
+ * @returns {{ seconds: number, human: string }} Numeric and formatted estimates.
43
+ */
44
+ function estimateCrackTime(entropy, guessesPerSecond = 1_000_000_000_000) {
45
+ const combinations = 2 ** entropy;
46
+ const averageSeconds = combinations / guessesPerSecond / 2;
47
+ return {
48
+ seconds: averageSeconds,
49
+ human: secondsToHuman(averageSeconds),
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Estimates the strength and cracking time for a password.
55
+ *
56
+ * @param {string} password Password to assess.
57
+ * @returns {{ score: number, label: string, entropy: number, crackTime: string | null, crackSeconds?: number }} Strength details.
58
+ */
59
+ export function passwordStrength(password) {
60
+ const value = password || '';
61
+ if (!value) {
62
+ return {
63
+ score: 0,
64
+ label: 'Empty',
65
+ entropy: 0,
66
+ crackTime: null,
67
+ };
68
+ }
69
+
70
+ const length = value.length;
71
+ let pool = 0;
72
+ if (/[a-z]/.test(value)) pool += 26;
73
+ if (/[A-Z]/.test(value)) pool += 26;
74
+ if (/\d/.test(value)) pool += 10;
75
+ if (/[^A-Za-z0-9]/.test(value)) pool += 33;
76
+
77
+ let entropy = Math.log2(pool || 1) * length;
78
+ if (/^(.)\1+$/.test(value)) entropy *= 0.1;
79
+ if (/123456|password|qwerty|letmein|admin|welcome/i.test(value)) entropy *= 0.3;
80
+ if (/(.)\1{2,}/.test(value)) entropy *= 0.8;
81
+ if (/^\d+$/.test(value)) entropy *= 0.5;
82
+
83
+ const crack = estimateCrackTime(entropy);
84
+ const [score, label] = scoreFromCrackSeconds(crack.seconds);
85
+ return {
86
+ score,
87
+ label,
88
+ entropy: Math.round(entropy),
89
+ crackTime: crack.human,
90
+ crackSeconds: crack.seconds,
91
+ };
92
+ }
@@ -0,0 +1,83 @@
1
+ <script setup>
2
+ import DomField from '../field/DomField.vue';
3
+ import { fieldProps } from '../field/fieldProps.js';
4
+ import { useField } from '../field/useField.js';
5
+
6
+ defineOptions({
7
+ __doc: {
8
+ name: 'Position input',
9
+ tag: '<DomPositionInput>',
10
+ description: 'A compact visual picker for popover and floating element positions.',
11
+ events: [{ name: 'update:modelValue', payload: 'string', description: 'Fired when the selected position changes.' }],
12
+ },
13
+ });
14
+
15
+ const props = defineProps({
16
+ ...fieldProps,
17
+ modelValue: {
18
+ type: String,
19
+ default: 'bottom-start',
20
+ _edit: { group: 'Control props', description: 'Selected position token.' },
21
+ },
22
+ });
23
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
24
+ const field = useField(props, emit, { idPrefix: 'dom-position-input' });
25
+
26
+ const positions = [
27
+ { value: 'start-bottom', label: 'Start bottom', class: 'left-0 top-3/4 -translate-x-1/2 -translate-y-1/2' },
28
+ { value: 'start', label: 'Start', class: 'left-0 top-1/2 -translate-x-1/2 -translate-y-1/2' },
29
+ { value: 'start-top', label: 'Start top', class: 'left-0 top-1/4 -translate-x-1/2 -translate-y-1/2' },
30
+ { value: 'top-start', label: 'Top start', class: 'left-1/4 top-0 -translate-x-1/2 -translate-y-1/2' },
31
+ { value: 'top', label: 'Top', class: 'left-1/2 top-0 -translate-x-1/2 -translate-y-1/2' },
32
+ { value: 'top-end', label: 'Top end', class: 'left-3/4 top-0 -translate-x-1/2 -translate-y-1/2' },
33
+ { value: 'end-top', label: 'End top', class: 'right-0 top-1/4 translate-x-1/2 -translate-y-1/2' },
34
+ { value: 'end', label: 'End', class: 'right-0 top-1/2 translate-x-1/2 -translate-y-1/2' },
35
+ { value: 'end-bottom', label: 'End bottom', class: 'right-0 top-3/4 translate-x-1/2 -translate-y-1/2' },
36
+ { value: 'bottom-end', label: 'Bottom end', class: 'left-3/4 bottom-0 -translate-x-1/2 translate-y-1/2' },
37
+ { value: 'bottom', label: 'Bottom', class: 'left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2' },
38
+ { value: 'bottom-start', label: 'Bottom start', class: 'left-1/4 bottom-0 -translate-x-1/2 translate-y-1/2' },
39
+ ];
40
+ </script>
41
+
42
+ <template>
43
+ <DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
44
+ <input
45
+ v-if="field.htmlName.value"
46
+ type="hidden"
47
+ :name="field.htmlName.value"
48
+ :value="field.value.value"
49
+ />
50
+ <div
51
+ role="group"
52
+ :aria-label="label || undefined"
53
+ :aria-describedby="field.describedBy.value || undefined"
54
+ class="relative h-32 rounded-xl border border-border bg-secondary/30 p-3"
55
+ >
56
+ <div class="absolute left-1/2 top-1/2 size-24 -translate-x-1/2 -translate-y-1/2">
57
+ <div class="absolute inset-0 rounded-lg border border-border bg-canvas shadow-sm"></div>
58
+ <button
59
+ v-for="position in positions"
60
+ :key="position.value"
61
+ type="button"
62
+ class="absolute grid size-5 place-items-center rounded-full border text-[0px] transition focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
63
+ :class="[
64
+ position.class,
65
+ field.value.value === position.value
66
+ ? 'border-primary bg-primary text-primary-fg shadow-sm'
67
+ : 'border-border bg-canvas text-muted-fg hover:border-ring hover:bg-accent',
68
+ ]"
69
+ :title="position.label"
70
+ :aria-label="position.label"
71
+ :aria-pressed="field.value.value === position.value"
72
+ :disabled="field.disabled.value || undefined"
73
+ @click="field.onInput(position.value)"
74
+ @focus="field.onFocus"
75
+ @blur="field.onBlur"
76
+ >
77
+ <span class="size-1.5 rounded-full bg-current"></span>
78
+ </button>
79
+ </div>
80
+ </div>
81
+ <p class="font-mono text-xs text-muted-fg">{{ field.value.value }}</p>
82
+ </DomField>
83
+ </template>
@@ -0,0 +1,102 @@
1
+ <script setup>
2
+ import { onMounted, ref, watch } from 'vue';
3
+ import DomField from '../field/DomField.vue';
4
+ import { fieldProps } from '../field/fieldProps.js';
5
+ import { useField } from '../field/useField.js';
6
+
7
+ defineOptions({
8
+ __doc: {
9
+ name: 'Radio group',
10
+ tag: '<DomRadioGroup>',
11
+ description: 'A single-select radio group with roving focus and full styling control.',
12
+ slots: [{ name: 'option', payload: '{ option, index }', description: 'Custom option markup.' }],
13
+ events: [{ name: 'update:modelValue', payload: '(value: string)', description: 'Emitted when selection changes.' }],
14
+ },
15
+ });
16
+
17
+ const props = defineProps({
18
+ ...fieldProps,
19
+ modelValue: {
20
+ type: [String, Number],
21
+ default: '',
22
+ _edit: { group: 'Control props', description: 'Selected value.' },
23
+ },
24
+ options: {
25
+ type: Array,
26
+ required: true,
27
+ _edit: {
28
+ component: 'DomJsonListInput',
29
+ description: 'Radio options.',
30
+ props: {
31
+ compact: true,
32
+ addLabel: '+ Add option',
33
+ schema: [
34
+ { key: 'label', label: 'Label', placeholder: 'Option label', default: (index) => `Option ${index + 1}` },
35
+ { key: 'value', label: 'Value', placeholder: 'option-value', default: (index) => `option-${index + 1}` },
36
+ ],
37
+ },
38
+ },
39
+ },
40
+ orientation: {
41
+ type: String,
42
+ default: 'vertical',
43
+ _edit: { group: 'Control props', options: ['vertical', 'horizontal'], description: 'Arrow key direction.' },
44
+ },
45
+ });
46
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
47
+ const root = ref(null);
48
+ const field = useField(props, emit, { idPrefix: 'dom-radio-group' });
49
+
50
+ onMounted(async () => {
51
+ await import('../../lib/headless/radio-group.js');
52
+ root.value?.addEventListener('dom:change', (event) => field.onInput(event.detail.value));
53
+ });
54
+
55
+ watch(field.value, (value) => {
56
+ if (root.value && String(root.value.value) !== String(value ?? '')) root.value.value = value ?? '';
57
+ });
58
+
59
+ const labelOf = (option) => (option && typeof option === 'object' ? (option.label ?? option.value) : option);
60
+ const valueOf = (option) => String(option && typeof option === 'object' ? (option.value ?? option.label) : option);
61
+ const selected = (option) => String(field.value.value ?? '') === valueOf(option);
62
+ </script>
63
+
64
+ <template>
65
+ <DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
66
+ <input
67
+ v-if="field.htmlName.value"
68
+ type="hidden"
69
+ :name="field.htmlName.value"
70
+ :value="field.value.value"
71
+ />
72
+ <dom-radio-group
73
+ ref="root"
74
+ :id="field.id.value"
75
+ :value="field.value.value ?? ''"
76
+ :orientation="orientation"
77
+ :aria-label="label || undefined"
78
+ :aria-describedby="field.describedBy.value || undefined"
79
+ :aria-invalid="field.invalid.value || undefined"
80
+ :aria-errormessage="field.errorId.value || undefined"
81
+ class="grid gap-2 data-[invalid]:rounded-2xl data-[invalid]:ring-1 data-[invalid]:ring-destructive"
82
+ :class="orientation === 'horizontal' && 'grid-flow-col justify-start'"
83
+ :data-invalid="field.invalid.value ? '' : undefined"
84
+ >
85
+ <button
86
+ v-for="(option, index) in options"
87
+ :key="valueOf(option) || index"
88
+ role="radio"
89
+ :data-value="valueOf(option)"
90
+ :disabled="field.disabled.value || undefined"
91
+ class="inline-flex items-center gap-3 rounded-xl border border-border bg-canvas px-3 py-2 text-left text-sm text-canvas-fg outline-none transition hover:bg-secondary focus:ring-2 focus:ring-ring/40 aria-checked:border-primary aria-checked:bg-secondary disabled:cursor-not-allowed disabled:opacity-50"
92
+ @focus="field.onFocus"
93
+ @blur="field.onBlur"
94
+ >
95
+ <span class="grid size-4 place-items-center rounded-full border border-border" aria-hidden="true">
96
+ <span class="size-2 rounded-full bg-primary" :class="selected(option) ? 'opacity-100' : 'opacity-0'"></span>
97
+ </span>
98
+ <slot name="option" :option="option" :index="index">{{ labelOf(option) }}</slot>
99
+ </button>
100
+ </dom-radio-group>
101
+ </DomField>
102
+ </template>