@crockery/fellowship-components 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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/RELEASING.md +75 -0
  4. package/THIRD_PARTY_LICENSES.md +79 -0
  5. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  6. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  7. package/dist/assets/ibm-plex-sans-condensed-latin-400-normal-DKyD6XsY.woff2 +0 -0
  8. package/dist/assets/ibm-plex-sans-condensed-latin-500-normal-BqaLkUoX.woff2 +0 -0
  9. package/dist/assets/ibm-plex-sans-condensed-latin-600-normal-CRd5VyFf.woff2 +0 -0
  10. package/dist/assets/ibm-plex-sans-condensed-latin-700-normal-D8r4s4aS.woff2 +0 -0
  11. package/dist/chunks/data-source-CoIFvV72.js +10 -0
  12. package/dist/chunks/data-source-CoIFvV72.js.map +1 -0
  13. package/dist/chunks/decorate-B_ZTN9li.js +92 -0
  14. package/dist/chunks/decorate-B_ZTN9li.js.map +1 -0
  15. package/dist/chunks/dungeon-map-IPN6sEDj.js +1144 -0
  16. package/dist/chunks/dungeon-map-IPN6sEDj.js.map +1 -0
  17. package/dist/chunks/talent-calculator-Ch1Pbbba.js +1086 -0
  18. package/dist/chunks/talent-calculator-Ch1Pbbba.js.map +1 -0
  19. package/dist/chunks/tooltip-DoQVSnOn.js +412 -0
  20. package/dist/chunks/tooltip-DoQVSnOn.js.map +1 -0
  21. package/dist/data/data-source.d.ts +17 -0
  22. package/dist/data/data-source.d.ts.map +1 -0
  23. package/dist/data/package-data-source.d.ts +6 -0
  24. package/dist/data/package-data-source.d.ts.map +1 -0
  25. package/dist/data/package-talent-data-source.d.ts +6 -0
  26. package/dist/data/package-talent-data-source.d.ts.map +1 -0
  27. package/dist/data/talent-data-source.d.ts +11 -0
  28. package/dist/data/talent-data-source.d.ts.map +1 -0
  29. package/dist/dungeon-map/dungeon-map.d.ts +56 -0
  30. package/dist/dungeon-map/dungeon-map.d.ts.map +1 -0
  31. package/dist/dungeon-map/dungeon-map.styles.d.ts +2 -0
  32. package/dist/dungeon-map/dungeon-map.styles.d.ts.map +1 -0
  33. package/dist/dungeon-map/index.d.ts +5 -0
  34. package/dist/dungeon-map/index.d.ts.map +1 -0
  35. package/dist/dungeon-map/layout.d.ts +3 -0
  36. package/dist/dungeon-map/layout.d.ts.map +1 -0
  37. package/dist/dungeon-map/render-model.d.ts +7 -0
  38. package/dist/dungeon-map/render-model.d.ts.map +1 -0
  39. package/dist/dungeon-map/types.d.ts +107 -0
  40. package/dist/dungeon-map/types.d.ts.map +1 -0
  41. package/dist/dungeon-map/viewport-controller.d.ts +13 -0
  42. package/dist/dungeon-map/viewport-controller.d.ts.map +1 -0
  43. package/dist/dungeon-map.js +7 -0
  44. package/dist/dungeon-map.js.map +1 -0
  45. package/dist/index.d.ts +9 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +8 -0
  48. package/dist/react/dungeon-map.d.ts +22 -0
  49. package/dist/react/dungeon-map.d.ts.map +1 -0
  50. package/dist/react/dungeon-map.js +180 -0
  51. package/dist/react/dungeon-map.js.map +1 -0
  52. package/dist/react/index.d.ts +7 -0
  53. package/dist/react/index.d.ts.map +1 -0
  54. package/dist/react/talent-calculator.d.ts +22 -0
  55. package/dist/react/talent-calculator.d.ts.map +1 -0
  56. package/dist/react/talent-calculator.js +71 -0
  57. package/dist/react/talent-calculator.js.map +1 -0
  58. package/dist/react/tooltip.d.ts +15 -0
  59. package/dist/react/tooltip.d.ts.map +1 -0
  60. package/dist/react/tooltip.js +26 -0
  61. package/dist/react/tooltip.js.map +1 -0
  62. package/dist/react.js +4 -0
  63. package/dist/styles/field-manual-fonts.d.ts +3 -0
  64. package/dist/styles/field-manual-fonts.d.ts.map +1 -0
  65. package/dist/styles/field-manual.styles.d.ts +3 -0
  66. package/dist/styles/field-manual.styles.d.ts.map +1 -0
  67. package/dist/talent-calculator/index.d.ts +5 -0
  68. package/dist/talent-calculator/index.d.ts.map +1 -0
  69. package/dist/talent-calculator/render-model.d.ts +5 -0
  70. package/dist/talent-calculator/render-model.d.ts.map +1 -0
  71. package/dist/talent-calculator/selection-state.d.ts +10 -0
  72. package/dist/talent-calculator/selection-state.d.ts.map +1 -0
  73. package/dist/talent-calculator/talent-calculator.d.ts +51 -0
  74. package/dist/talent-calculator/talent-calculator.d.ts.map +1 -0
  75. package/dist/talent-calculator/talent-calculator.styles.d.ts +2 -0
  76. package/dist/talent-calculator/talent-calculator.styles.d.ts.map +1 -0
  77. package/dist/talent-calculator/types.d.ts +92 -0
  78. package/dist/talent-calculator/types.d.ts.map +1 -0
  79. package/dist/talent-calculator.js +7 -0
  80. package/dist/talent-calculator.js.map +1 -0
  81. package/dist/tooltip/index.d.ts +6 -0
  82. package/dist/tooltip/index.d.ts.map +1 -0
  83. package/dist/tooltip/positioning.d.ts +23 -0
  84. package/dist/tooltip/positioning.d.ts.map +1 -0
  85. package/dist/tooltip/rich-text.d.ts +4 -0
  86. package/dist/tooltip/rich-text.d.ts.map +1 -0
  87. package/dist/tooltip/tooltip.d.ts +33 -0
  88. package/dist/tooltip/tooltip.d.ts.map +1 -0
  89. package/dist/tooltip/tooltip.styles.d.ts +2 -0
  90. package/dist/tooltip/tooltip.styles.d.ts.map +1 -0
  91. package/dist/tooltip/types.d.ts +19 -0
  92. package/dist/tooltip/types.d.ts.map +1 -0
  93. package/dist/tooltip.js +7 -0
  94. package/dist/tooltip.js.map +1 -0
  95. package/package.json +149 -0
  96. package/src/dungeon-map/README.md +168 -0
  97. package/src/talent-calculator/README.md +190 -0
  98. package/src/tooltip/README.md +113 -0
package/package.json ADDED
@@ -0,0 +1,149 @@
1
+ {
2
+ "name": "@crockery/fellowship-components",
3
+ "version": "0.1.0",
4
+ "description": "Lit web components for rendering Fellowship game data.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Crockery/fellowship-components.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/Crockery/fellowship-components/issues"
11
+ },
12
+ "homepage": "https://github.com/Crockery/fellowship-components#readme",
13
+ "keywords": [
14
+ "fellowship",
15
+ "web-components",
16
+ "lit",
17
+ "react",
18
+ "dungeon-map",
19
+ "talent-calculator",
20
+ "tooltip"
21
+ ],
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "sideEffects": [
25
+ "./dist/index.js",
26
+ "./dist/dungeon-map.js",
27
+ "./dist/talent-calculator.js",
28
+ "./dist/tooltip.js",
29
+ "./dist/react.js",
30
+ "./dist/react/*.js"
31
+ ],
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js",
39
+ "default": "./dist/index.js"
40
+ },
41
+ "./dungeon-map": {
42
+ "types": "./dist/dungeon-map/index.d.ts",
43
+ "import": "./dist/dungeon-map.js",
44
+ "default": "./dist/dungeon-map.js"
45
+ },
46
+ "./talent-calculator": {
47
+ "types": "./dist/talent-calculator/index.d.ts",
48
+ "import": "./dist/talent-calculator.js",
49
+ "default": "./dist/talent-calculator.js"
50
+ },
51
+ "./tooltip": {
52
+ "types": "./dist/tooltip/index.d.ts",
53
+ "import": "./dist/tooltip.js",
54
+ "default": "./dist/tooltip.js"
55
+ },
56
+ "./react": {
57
+ "types": "./dist/react/index.d.ts",
58
+ "import": "./dist/react.js",
59
+ "default": "./dist/react.js"
60
+ },
61
+ "./react/dungeon-map": {
62
+ "types": "./dist/react/dungeon-map.d.ts",
63
+ "import": "./dist/react/dungeon-map.js",
64
+ "default": "./dist/react/dungeon-map.js"
65
+ },
66
+ "./react/talent-calculator": {
67
+ "types": "./dist/react/talent-calculator.d.ts",
68
+ "import": "./dist/react/talent-calculator.js",
69
+ "default": "./dist/react/talent-calculator.js"
70
+ },
71
+ "./react/tooltip": {
72
+ "types": "./dist/react/tooltip.d.ts",
73
+ "import": "./dist/react/tooltip.js",
74
+ "default": "./dist/react/tooltip.js"
75
+ },
76
+ "./package.json": "./package.json"
77
+ },
78
+ "files": [
79
+ "dist",
80
+ "src/*/README.md",
81
+ "README.md",
82
+ "RELEASING.md",
83
+ "LICENSE",
84
+ "THIRD_PARTY_LICENSES.md"
85
+ ],
86
+ "publishConfig": {
87
+ "access": "public"
88
+ },
89
+ "engines": {
90
+ "node": "^20.19.0 || >=22.12.0"
91
+ },
92
+ "packageManager": "pnpm@11.3.0",
93
+ "scripts": {
94
+ "build": "vite build && tsc -p tsconfig.build.json",
95
+ "typecheck": "tsc -p tsconfig.json --noEmit",
96
+ "lint": "biome lint .",
97
+ "format": "biome format --write .",
98
+ "format:check": "biome format .",
99
+ "test": "pnpm test:unit && pnpm test:browser",
100
+ "test:unit": "vitest run --project unit",
101
+ "test:browser": "vitest run --project browser",
102
+ "test:browser:smoke": "FELLOWSHIP_TEST_BROWSERS=smoke vitest run --project browser -t '\\[smoke\\]'",
103
+ "test:browser:all": "FELLOWSHIP_TEST_BROWSERS=all vitest run --project browser",
104
+ "ladle": "ladle serve",
105
+ "ladle:build": "ladle build",
106
+ "package:lint": "publint --strict --pack npm && attw --pack . --profile esm-only",
107
+ "pack:check": "pnpm build && node scripts/check-build.mjs && pnpm package:lint && pnpm pack --dry-run",
108
+ "prepublishOnly": "pnpm build && node scripts/check-build.mjs && pnpm package:lint",
109
+ "changeset": "changeset",
110
+ "version-packages": "changeset version",
111
+ "release": "changeset publish"
112
+ },
113
+ "dependencies": {
114
+ "@crockery/fellowship-data": "^1.0.0",
115
+ "@lit/react": "^1.0.8",
116
+ "lit": "^3.3.3"
117
+ },
118
+ "peerDependencies": {
119
+ "react": ">=18 <20",
120
+ "react-dom": ">=18 <20"
121
+ },
122
+ "peerDependenciesMeta": {
123
+ "react": {
124
+ "optional": true
125
+ },
126
+ "react-dom": {
127
+ "optional": true
128
+ }
129
+ },
130
+ "devDependencies": {
131
+ "@biomejs/biome": "^2.5.3",
132
+ "@changesets/cli": "^2.31.0",
133
+ "@fontsource/ibm-plex-mono": "5.2.7",
134
+ "@fontsource/ibm-plex-sans-condensed": "5.2.8",
135
+ "@arethetypeswrong/cli": "^0.18.5",
136
+ "@ladle/react": "^5.1.1",
137
+ "@types/react": "^18.3.24",
138
+ "@types/react-dom": "^18.3.7",
139
+ "@vitejs/plugin-react-swc": "^4.3.1",
140
+ "@vitest/browser-playwright": "^4.1.10",
141
+ "playwright": "^1.61.1",
142
+ "publint": "^0.3.21",
143
+ "react": "^18.3.1",
144
+ "react-dom": "^18.3.1",
145
+ "typescript": "^5.9.3",
146
+ "vite": "^8.1.4",
147
+ "vitest": "^4.1.10"
148
+ }
149
+ }
@@ -0,0 +1,168 @@
1
+ # Dungeon map
2
+
3
+ `<fellowship-dungeon-map>` draws a Fellowship dungeon map to canvas and places enemy and group
4
+ HTML in a synchronized, interactive DOM overlay.
5
+
6
+ ## Import and basic usage
7
+
8
+ Importing the component entry point registers the custom element:
9
+
10
+ ```ts
11
+ import "@crockery/fellowship-components/dungeon-map";
12
+ ```
13
+
14
+ ```html
15
+ <fellowship-dungeon-map
16
+ dungeon-id="Level.ID.DemonicRitual"
17
+ map-image-id="Level.ID.DemonicRitual:T_Map_Demonicritual_Start"
18
+ ></fellowship-dungeon-map>
19
+ ```
20
+
21
+ The default data source lazily loads dungeons, enemies, and the manifest from
22
+ `@crockery/fellowship-data`. When `mapImageId` is absent, the first upstream map image is
23
+ selected.
24
+
25
+ ## Lit renderers
26
+
27
+ Renderer functions receive upstream enemy placement data plus normalized group geometry. Group
28
+ children are already position-aware and only need to be wrapped:
29
+
30
+ ```ts
31
+ import { html } from "lit";
32
+ import type { FellowshipDungeonMapElement } from "@crockery/fellowship-components/dungeon-map";
33
+
34
+ const map = document.querySelector<FellowshipDungeonMapElement>("fellowship-dungeon-map")!;
35
+
36
+ map.renderEnemy = ({ model }) => html` <span>${model.enemy?.name ?? model.placement.id}</span> `;
37
+
38
+ map.renderGroup = ({ group, children }) => html`
39
+ <div class="pull" data-group=${group.id}>${children}</div>
40
+ `;
41
+ ```
42
+
43
+ `LitEnemyRenderer` receives `{ model, group, dungeon, mapImage }`.
44
+ `LitGroupRenderer` receives `{ group, children, dungeon, mapImage }`, where `children`
45
+ contains positioned enemy anchors. Both return a Lit `TemplateResult` or `nothing`.
46
+
47
+ ## React
48
+
49
+ ```tsx
50
+ import { DungeonMapViewer } from "@crockery/fellowship-components/react/dungeon-map";
51
+
52
+ export function DungeonRoute() {
53
+ return (
54
+ <DungeonMapViewer
55
+ dungeonId="Level.ID.DemonicRitual"
56
+ renderEnemy={({ model }) => <EnemyMarker enemy={model.enemy} />}
57
+ renderGroup={({ group, children }) => <PullGroup group={group}>{children}</PullGroup>}
58
+ onMapImageChange={(event) => console.log(event.detail.mapImageId)}
59
+ />
60
+ );
61
+ }
62
+ ```
63
+
64
+ Custom React nodes render through a React-owned light-DOM overlay while the Lit element retains
65
+ map loading, canvas drawing, positioning, and viewport interactions.
66
+
67
+ ## Data and assets
68
+
69
+ Set `assetBaseUrl` to self-host package images:
70
+
71
+ ```ts
72
+ map.assetBaseUrl = "https://static.example.com/fellowship-data/v1.0.0/";
73
+ ```
74
+
75
+ Replace `dataSource` for filtered, experimental, remote, or application-owned data:
76
+
77
+ ```ts
78
+ import type { FellowshipDataSource } from "@crockery/fellowship-components/dungeon-map";
79
+
80
+ declare const applicationData: FellowshipDataSource;
81
+
82
+ const source: FellowshipDataSource = {
83
+ async loadDungeon(dungeonId, signal) {
84
+ return applicationData.loadDungeon(dungeonId, signal);
85
+ },
86
+ resolveAssetUrl(asset) {
87
+ return new URL(asset.path, "https://static.example.com/").href;
88
+ },
89
+ };
90
+
91
+ map.dataSource = source;
92
+ ```
93
+
94
+ ## API
95
+
96
+ ### Properties
97
+
98
+ - `dungeonId: string`
99
+ - `mapImageId: string | null`
100
+ - `assetBaseUrl: string | null` — `null` uses the package-pinned CDN.
101
+ - `dataSource: FellowshipDataSource | null` — `null` uses the package provider.
102
+ - `renderEnemy: LitEnemyRenderer | null`
103
+ - `renderGroup: LitGroupRenderer | null`
104
+ - `mapLabelFormatter: MapLabelFormatter | null`
105
+ - `showControls: boolean` — defaults to `true`.
106
+ - `showMapSelector: boolean` — defaults to `true`.
107
+ - `maxZoom: number` — defaults to `8`.
108
+
109
+ The React wrapper treats `mapImageId` as hybrid controlled state: an initial value selects the
110
+ starting floor, user selections update the rendered map immediately, and later prop changes still
111
+ replace the selection.
112
+
113
+ ### Methods
114
+
115
+ - `fitToView()`
116
+ - `zoomIn()`
117
+ - `zoomOut()`
118
+ - `retry()`
119
+
120
+ ### Events
121
+
122
+ - `fellowship-map-ready`
123
+ - `fellowship-map-error`
124
+ - `fellowship-map-warning`
125
+ - `fellowship-map-image-change`
126
+ - `fellowship-map-viewport-change`
127
+
128
+ All events bubble across the shadow boundary and carry typed `detail` payloads.
129
+
130
+ ## Interaction and accessibility
131
+
132
+ - Mouse-wheel zoom stays anchored beneath the pointer.
133
+ - Pointer drag and arrow keys pan the map.
134
+ - Two touch pointers pinch to zoom.
135
+ - `+`, `-`, and `0` zoom in, zoom out, and fit.
136
+ - The map selector uses a tablist with arrow, Home, and End navigation.
137
+ - Initial load, map changes, and resize fit the map to the viewport.
138
+ - Reduced-motion preferences remove marker and control transitions.
139
+
140
+ ## Theming
141
+
142
+ ```css
143
+ fellowship-dungeon-map {
144
+ --fellowship-map-soot: #0b0f13;
145
+ --fellowship-map-ink: #f1eee5;
146
+ --fellowship-map-muted-ink: #b7b4aa;
147
+ --fellowship-map-border: rgb(241 238 229 / 10%);
148
+ --fellowship-map-focus: #a8e6ee;
149
+ --fellowship-map-rune: #69c4d4;
150
+ --fellowship-map-ember: #d9784c;
151
+ --fellowship-map-control-surface: rgb(21 28 35 / 92%);
152
+ --fellowship-map-enemy-size: 32px;
153
+ --fellowship-map-font-family: "Fellowship Field", sans-serif;
154
+ --fellowship-map-tactical-font-family: "Fellowship Tactical", monospace;
155
+ --fellowship-map-radius: 6px;
156
+ }
157
+
158
+ fellowship-dungeon-map::part(control-button) {
159
+ font-weight: 800;
160
+ }
161
+ ```
162
+
163
+ CSS parts: `viewport`, `map-canvas`, `overlay`, `group`, `enemy`, `controls`,
164
+ `control-button`, `map-selector`, `map-selector-button`, and `status`.
165
+
166
+ The map also inherits all shared `--fellowship-field-*` tokens. Component tokens above take
167
+ precedence. Compact control rails respond to the map element's container width, so narrow embedded
168
+ maps adapt even when the browser viewport is wide.
@@ -0,0 +1,190 @@
1
+ # Talent calculator
2
+
3
+ `<fellowship-talent-calculator>` renders a hero's six-row talent grid and manages an affordable
4
+ selection against a configurable point budget.
5
+
6
+ ## Import and basic usage
7
+
8
+ Importing the entry point registers the calculator and its tooltip dependency:
9
+
10
+ ```ts
11
+ import "@crockery/fellowship-components/talent-calculator";
12
+ ```
13
+
14
+ ```html
15
+ <fellowship-talent-calculator hero-id="firemage" talent-points="14"> </fellowship-talent-calculator>
16
+ ```
17
+
18
+ The default provider lazily loads heroes, talents, and the manifest from
19
+ `@crockery/fellowship-data`. Missing or failed talent images use deterministic rune fallbacks.
20
+
21
+ ## Point budget and selection
22
+
23
+ `talentPoints` is the total available budget, not the remaining point count. The header displays
24
+ `spent / available`.
25
+
26
+ - Any affordable combination may be selected, including multiple talents from one row.
27
+ - There are no tier prerequisites or one-per-row restrictions.
28
+ - Unaffordable talents remain focusable for tooltip and requirement discovery.
29
+ - Selected talents can always be removed.
30
+ - The **Clear All** button removes every selected talent without changing the point budget.
31
+ - Point controls normalize the total to an integer from `0` through `14`.
32
+ - The total cannot be decreased below currently spent points.
33
+
34
+ The element is hybrid controlled. User actions update `selectedTalentIds` and `talentPoints`
35
+ before events are emitted, and applications may set either property directly.
36
+
37
+ Externally supplied over-budget selections are preserved and marked invalid. New choices remain
38
+ blocked until the state is valid, while deselection and point increases are still available.
39
+ Unknown, duplicate, or inactive supplied IDs are removed after data loads.
40
+
41
+ ## Lit API
42
+
43
+ ```ts
44
+ import type {
45
+ FellowshipTalentCalculatorElement,
46
+ TalentSelectionChangeEvent,
47
+ } from "@crockery/fellowship-components/talent-calculator";
48
+
49
+ const calculator = document.querySelector<FellowshipTalentCalculatorElement>(
50
+ "fellowship-talent-calculator",
51
+ )!;
52
+
53
+ calculator.selectedTalentIds = ["firemage-talent-id"];
54
+ calculator.addEventListener(
55
+ "fellowship-talent-selection-change",
56
+ (event: TalentSelectionChangeEvent) => {
57
+ console.log(event.detail.selectedTalentIds, event.detail.remainingPoints);
58
+ },
59
+ );
60
+ ```
61
+
62
+ ### Custom talent content
63
+
64
+ The element owns each card button, talent image, point cost, tooltip, positioning, focus, and
65
+ selection behavior. A renderer changes the label presentation inside that shell and must not
66
+ return nested interactive controls. The default talent image remains visible; hide or restyle it
67
+ with `::part(talent-icon)` when supplying a complete custom presentation:
68
+
69
+ ```ts
70
+ import { html } from "lit";
71
+
72
+ calculator.renderTalent = ({ talent, selected }) => html`
73
+ <span class=${selected ? "chosen" : "available"}>
74
+ ${talent.row}.${talent.column} · ${talent.talent.name}
75
+ </span>
76
+ `;
77
+ ```
78
+
79
+ ## React
80
+
81
+ ```tsx
82
+ import { useState } from "react";
83
+ import { TalentCalculator } from "@crockery/fellowship-components/react/talent-calculator";
84
+
85
+ export function HeroBuild() {
86
+ const [selected, setSelected] = useState<readonly string[]>([]);
87
+
88
+ return (
89
+ <TalentCalculator
90
+ heroId="firemage"
91
+ talentPoints={14}
92
+ selectedTalentIds={selected}
93
+ onSelectionChange={(event) => setSelected(event.detail.selectedTalentIds)}
94
+ renderTalent={({ talent }) => <span>{talent.talent.name}</span>}
95
+ />
96
+ );
97
+ }
98
+ ```
99
+
100
+ React talent renderers mount presentation-only nodes into stable named slots. The web component
101
+ continues to own button semantics and selection state.
102
+
103
+ ## Data and assets
104
+
105
+ Self-host images with `assetBaseUrl`, or replace the complete provider:
106
+
107
+ ```ts
108
+ import type { FellowshipTalentDataSource } from "@crockery/fellowship-components/talent-calculator";
109
+
110
+ declare const applicationTalentData: FellowshipTalentDataSource;
111
+
112
+ calculator.assetBaseUrl = "https://static.example.com/fellowship-data/v1.0.0/";
113
+
114
+ const source: FellowshipTalentDataSource = {
115
+ async loadHeroTalents(heroId, signal) {
116
+ return applicationTalentData.loadHeroTalents(heroId, signal);
117
+ },
118
+ resolveAssetUrl(asset) {
119
+ return new URL(asset.path, "https://static.example.com/").href;
120
+ },
121
+ };
122
+
123
+ calculator.dataSource = source;
124
+ ```
125
+
126
+ ## API
127
+
128
+ ### Properties
129
+
130
+ - `heroId: string`
131
+ - `talentPoints: number` — defaults to `14`, normalized from `0` through
132
+ `MAX_TALENT_POINTS`.
133
+ - `selectedTalentIds: readonly string[]`
134
+ - `assetBaseUrl: string | null`
135
+ - `dataSource: FellowshipTalentDataSource | null`
136
+ - `renderTalent: LitTalentRenderer | null`
137
+ - `showPointControls: boolean` — defaults to `true`.
138
+ - `showTooltips: boolean` — defaults to `true`.
139
+
140
+ ### Methods
141
+
142
+ - `selectTalent(talentId): boolean`
143
+ - `deselectTalent(talentId): boolean`
144
+ - `reset()` — clears selections without changing the point total.
145
+ - `retry()`
146
+
147
+ ### Events
148
+
149
+ - `fellowship-talents-ready`
150
+ - `fellowship-talents-error`
151
+ - `fellowship-talents-warning`
152
+ - `fellowship-talent-selection-change`
153
+ - `fellowship-talent-points-change`
154
+
155
+ All events bubble across the shadow boundary and carry typed details. Selection events report
156
+ `selectedTalentIds`, `spentPoints`, `remainingPoints`, and their change source.
157
+
158
+ ## Accessibility
159
+
160
+ Rows are semantic fieldsets with legends. Cards are native buttons with `aria-pressed`, accessible
161
+ descriptions, visible focus rings, and focusable `aria-disabled` states. Clear All and the point
162
+ controls are native buttons with visible focus states and 44px interaction targets. Status changes
163
+ use polite live regions; newly raised errors use assertive announcements.
164
+
165
+ ## Theming
166
+
167
+ The calculator inherits the shared `--fellowship-field-*` theme described in the package README.
168
+ Its existing component tokens take precedence, so established themes remain compatible.
169
+
170
+ ```css
171
+ fellowship-talent-calculator {
172
+ --fellowship-talent-background: #0b0f13;
173
+ --fellowship-talent-surface: #10161c;
174
+ --fellowship-talent-card-background: #10161c;
175
+ --fellowship-talent-selection: #69c4d4;
176
+ --fellowship-talent-points: #c1a36a;
177
+ --fellowship-talent-danger: #d9784c;
178
+ --fellowship-talent-icon-size: 56px;
179
+ }
180
+ ```
181
+
182
+ CSS parts: `container`, `header`, `heading`, `points`, `points-spent`, `points-total`,
183
+ `point-controls`, `point-control`, `clear-all`, `grid`, `row`, `row-label`, `talent`,
184
+ `talent-icon`, `talent-icon-fallback`, `talent-name`, `talent-cost`, `status`,
185
+ `empty-state`, and `error-state`.
186
+
187
+ Design tokens use the `--fellowship-talent-*` prefix and cover the background and surfaces,
188
+ foreground hierarchy, disabled state, border, selection, focus, point accent, danger state,
189
+ control surface, icon size, interface and tactical font families, and radius. The compact
190
+ single-column layout is activated by the component's own container width rather than the viewport.
@@ -0,0 +1,113 @@
1
+ # Tooltip
2
+
3
+ `<fellowship-tooltip>` provides accessible, collision-aware Fellowship tooltips with a safe
4
+ rich-text resolver.
5
+
6
+ ## Import and basic usage
7
+
8
+ ```ts
9
+ import "@crockery/fellowship-components/tooltip";
10
+ ```
11
+
12
+ ```html
13
+ <fellowship-tooltip
14
+ heading="Runic Convergence"
15
+ text="Gain <rt.absorb>20 absorb</> and restore <rt.mana>5 mana</>."
16
+ >
17
+ <button type="button">Inspect talent</button>
18
+ </fellowship-tooltip>
19
+ ```
20
+
21
+ The default slot contains the trigger. Supply fully custom tooltip content through the named
22
+ `content` slot.
23
+
24
+ ## React
25
+
26
+ ```tsx
27
+ import { Tooltip } from "@crockery/fellowship-components/react/tooltip";
28
+
29
+ export function Help() {
30
+ return (
31
+ <Tooltip heading="Talent points" text="Points can be reassigned at any time.">
32
+ <button type="button">Help</button>
33
+ </Tooltip>
34
+ );
35
+ }
36
+ ```
37
+
38
+ Pass a React node to the wrapper's `content` prop to fill the named content slot.
39
+
40
+ ## Interaction and positioning
41
+
42
+ - Pointer hover opens after `openDelay`, which defaults to `250` milliseconds.
43
+ - Keyboard focus opens immediately.
44
+ - Pointer leave, focus leaving the host, or Escape closes the tooltip.
45
+ - `placement` accepts `auto`, `top`, `right`, `bottom`, or `left`.
46
+ - Automatic placement prefers top, then bottom, right, and left.
47
+ - The surface repositions on scroll, viewport resize, trigger resize, and content resize.
48
+ - Positioning maintains an 8px trigger gap and viewport collision margin.
49
+ - The native Popover API is used when available, with a fixed-position fallback.
50
+
51
+ Tooltip content is noninteractive and uses `role="tooltip"`. The first slotted trigger receives a
52
+ plain-text `aria-description` while connected; any existing value is restored afterward.
53
+
54
+ ## API
55
+
56
+ ### Properties
57
+
58
+ - `heading: string | null`
59
+ - `text: string | null`
60
+ - `placement: "auto" | "top" | "right" | "bottom" | "left"`
61
+ - `openDelay: number`
62
+ - `disabled: boolean`
63
+
64
+ ### Methods
65
+
66
+ - `show()`
67
+ - `hide()`
68
+
69
+ ### Event
70
+
71
+ `fellowship-tooltip-toggle` bubbles across the shadow boundary. Its detail contains `open` and
72
+ the reason: `pointer`, `focus`, `escape`, `programmatic`, or `disabled`.
73
+
74
+ ## Fellowship rich text
75
+
76
+ Descriptions are parsed into inert Lit text bindings. The component never uses `unsafeHTML` or
77
+ executes arbitrary markup.
78
+
79
+ Supported tags:
80
+
81
+ - `bold` and `rt.bold`
82
+ - `rt.absorb`
83
+ - `rt.warning`
84
+ - `rt.effect`
85
+ - `rt.mana`
86
+ - `rt.heal`
87
+ - `rt.meikoability1`
88
+ - `rt.meikoability2`
89
+
90
+ Both `</>` and named closing tags are supported. Unknown tags keep their text without styling,
91
+ and malformed or unclosed markup recovers safely. Paragraphs and line breaks are preserved.
92
+
93
+ The entry point also exports:
94
+
95
+ ```ts
96
+ import {
97
+ resolveFellowshipRichText,
98
+ stripFellowshipRichText,
99
+ } from "@crockery/fellowship-components/tooltip";
100
+ ```
101
+
102
+ ## Theming
103
+
104
+ The tooltip inherits the shared `--fellowship-field-*` theme and exposes the established
105
+ `--fellowship-tooltip-*` overrides. Its desaturated semantic defaults are ability `#76C5D2`,
106
+ warning `#D4A761`, effect `#AFA1CA`, resource `#7CB2C2`, healing `#82B997`, Meiko primary
107
+ `#C892A0`, and Meiko secondary `#8DBAB7`.
108
+
109
+ CSS parts: `trigger`, `surface`, `arrow`, `heading`, `content`, and `paragraph`.
110
+
111
+ Design tokens use the `--fellowship-tooltip-*` prefix and cover surface, foreground, muted text,
112
+ border, focus, semantic rich-text tones, font family, radius, and maximum width. Reduced-motion
113
+ preferences remove translated transitions.