Skip to content

v0.1.0

Latest

Choose a tag to compare

@github-actions github-actions released this 25 Jul 02:11

HelloGear

v0.1.0 (2026-07-25)

Full Changelog Previous Releases

  • Add release packaging
    Same shape as the sibling addons: a tag matching v* triggers the BigWigs
    packager, which builds the zip, generates a changelog from the commits
    since the last tag, creates a GitHub Release, and uploads to CurseForge
    when CF_API_KEY is present in repo secrets.
    The workflow checks the tag against the TOC's version before packaging.
    The TOC carries a literal version rather than @Project-Version@ so a copy
    loaded straight from a checkout shows a real number in game, which means
    it can drift from the tag - better to fail than to publish a zip whose
    in-game version is wrong.
    .pkgmeta keeps the dev-only material out of the zip: the headless test
    harnesses, the lint config, the docs, and the CurseForge project image.
    Icon.png ships because the TOC's IconTexture points at it, and LICENSE
    ships because the MIT terms ask for the notice to travel with copies.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Add the CurseForge project ID
    Placed as the siblings do, after Category.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Group the panel buttons by what they act on
    Undress moves up beside Equip, where it belongs: both act on the character
    right now. That leaves three pairs, each row doing one kind of thing -
    Equip/Undress on the character, Save/New set on sets, From/To bank on the
    bank - instead of an arbitrary split between a top row and a footer.
    New set comes up from the footer to fill the second row, which keeps the
    row count and so leaves the list exactly the height it was.
    The buttons also fit now: two at 83 plus a gap came to 170 in a 164-wide
    column and overhung into the scrollbar. Halving the column gives 80.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Add an undress button
    Takes everything off into your bags. The swap engine already knows how to
    empty a slot, so this is all of them at once and inherits the bag-space
    check and the retry loop rather than reimplementing either - gear stays on
    if there's nowhere to put it, and it says so.
    Sits beside New set in the footer, which shortens to fit; its tooltip
    carries what the longer label used to say. Also /hg undress.
    Deliberately not undoable. Recording it as a set's restore would mean
    silently overwriting one, which is worse than making the user equip a set
    to dress again.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Make every icon searchable by name
    Your dump came back as 1106 bare file IDs, so IconNames.lua maps them,
    generated from the community listfile (verified-listfile.csv) and filtered
    to exactly those - 49KB rather than the megabyte the full retail listfile
    would cost for no gain. Load is a table constructor; nothing is built until
    the picker first opens.
    Matching now happens on a form with every separator stripped. Names arrive
    from three places and none of them agree on spelling -
    spell_shadow_shadowbolt from the table, Chromatic Boots from an item,
    Shadow Bolt from a spell - and nobody types them consistently either, so
    "shadow bolt", "shadow_bolt" and "SHADOW BOLT" all find the same icons.
    Runtime naming from gear and spells stays: it needs no shipped data and
    covers whatever a later patch adds before the table is regenerated.
    test_iconsearch.lua runs the search key against the real shipped table.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Add /hg dumpicons to capture the client's icon list
    Groundwork for making every icon searchable. There is no API turning a
    file ID into a name, so the mapping has to be generated offline from the
    community listfile - and filtered to the icons this client actually has,
    or it means shipping tens of thousands of retail entries for a Classic
    addon.
    /hg dumpicons writes the raw list to saved variables; a reload puts it on
    disk where it can be read and turned into a table. Needed once, and again
    only if a patch adds icons - file IDs are stable, so an out-of-date table
    misses new icons rather than going wrong.
    The picker already looks for ns.IconNames when naming an icon, so the
    generated file drops straight in with no further wiring.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Widen icon search to everything with a name
    Search covered twenty icons because this client reports its macro icons as
    bare file IDs, which carry no name to match against - so only the set's own
    gear, named from the items themselves, was searchable.
    Everything else whose icon can be named now is: the rest of your gear,
    whatever's in your bags and the bank when it's open, and every spell you
    know. An icon shared with one of those is searchable by that thing's name,
    which is what you'd type looking for it anyway. The spellbook call differs
    across clients and neither form is guaranteed, so a miss there just means
    fewer names.
    The rest stay browsable but unsearchable. Covering them would need a file
    ID to name database shipped with the addon; the tooltip says as much
    rather than leaving the ceiling unexplained.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Make icon search work, and tidy the picker's placement
    Search asked the wrong client call first. There are two: the indexed one
    yields texture paths, whose last segment is a name, and the table-filling
    one yields bare file IDs, which carry nothing to match against. The
    table-filling call was tried first and returned successfully, so the
    indexed one was never reached and almost nothing had a name to search.
    Now the indexed call goes first and is only kept if it really does hand
    back strings.
    The header says what's actually searchable rather than leaving a box that
    quietly covers a fraction of the grid and reads as broken - the count of
    icons and, when they differ, how many have names. If none do, it says so
    and the box is disabled. SetEnabled also becomes Enable/Disable, which are
    the methods that certainly exist on an EditBox here.
    The picker sat further from the options panel than intended: both frames
    draw their borders inside their bounds, so the visible gap was the offset
    plus both insets. Offset accounts for that now.
    Closing the character sheet closes the picker, as does dismissing the
    options popout it hangs off - it belonged to that popout, and leaving it
    floating alone was debris.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Show sets with gear you don't have in red
    A set whose items aren't on you or in your bags now reads red in both the
    panel list and the quick menu, and its tooltip marks the offending slots
    rather than making you compare the list against your bags by eye.
    Anything the bank is holding is marked (bank) instead of (missing). Both
    are red - either way it isn't going on right now - but one of them tells
    you where to go. That distinction only exists while the bank window is
    open, since the client won't report its contents otherwise, and the
    tooltip degrades to "missing" when it can't ask.
    The lookup reduces to an item-and-suffix key so a set's slots are table
    lookups rather than a scan, and both lists build the inventory index once
    for the whole list instead of once per set. Eight cases cover it: worn and
    carried gear not counting, a re-enchant not counting, a different random
    suffix counting, and the bank distinction appearing and disappearing with
    the window.
    Also fixes the icon picker opening on top of the options panel that
    launched it - it sits beside it now - and drops "Use a set item", which
    the set's own icons coming first already covers.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Move sets to and from the bank
    The client only reports bank contents while the bank window is open, which
    shapes the feature: this can't be something equipping reaches for on its
    own, since the moment you want a set is the moment you're nowhere near a
    bank. So it's two explicit actions - buttons on the panel and /hg bank
    get|put - both disabled with an explanation unless the window is up.
    From bank moves the set's gear into your bags, skipping anything already
    on you or already carried. To bank moves it out of your bags and leaves
    worn gear alone; stripping a set off to put it away is what unequipping is
    for, and doing it implicitly would be a nasty surprise.
    The move loop follows the swap engine: recompute from the world each pass,
    move what can be moved, wait for the locks, go again. It gives up if the
    bank window closes underneath it.
    A swap that can't find something now says how many of the missing pieces
    are in the bank, when the bank is open to be asked.
    Four scenarios cover it against a simulated bank - withdrawing, skipping
    what you already have, depositing without stripping worn gear, and the
    window being shut.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Drop the key bindings
    Removes Bindings.xml, the BINDING_NAME globals, /hg bind and the
    per-character table behind it, along with the set rename and delete paths
    that kept that table in step.
    HelloGear.ToggleMenu goes with them - it existed only as the binding's
    entry point, and the minimap button and a bare /hg both already open the
    menu. The macro API is untouched, so a macro on an action bar still gets
    you a keypress if you want one.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Add an icon picker for sets
    Sets could only take the icon suggested from their own gear. The gear
    button's options now open a picker: an 8x7 grid, mouse wheel or slider to
    move through it, click to choose.
    The set's own item icons come first. Nine times in ten the icon you want
    is something the set puts on, and finding it in a grid of two thousand
    otherwise means scrolling for it. The client's macro icon list follows,
    fetched through whichever call this client has - modern fills a table,
    older ones hand back one at a time, and neither is guaranteed. If neither
    exists the picker still offers the set's own icons rather than failing.
    The grid opens on the row holding the set's current icon, which is marked,
    so it starts where the answer probably is. "Use a set item" keeps the old
    suggest-from-gear behaviour.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Stop Save from discarding slot edits
    Save replaces the set with whatever the character is wearing. That is what
    it is for, but it sat one click away from the slot editor, so editing a
    set and then pressing Save threw away every choice just made - which reads
    exactly as the edits not having been saved at all.
    The edits were already stored: slot changes are written into the set as
    they happen, so there was nothing to save in the first place. Save is now
    disabled while a set is being edited, labelled "Saved", and its tooltip
    says why. The status line says changes save as you go.
    Test covers the replace-not-merge behaviour the guard depends on: a
    hand-picked slot is overwritten from the character, unmanaged slots are
    added, and a deliberate empty is dropped.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Bake in the calibrated panel gap
    /hg dock -5 against a gap of 6 puts the right value at 1: the borders read
    as adjacent rather than joined, without the panel looking flung off to the
    side.
    The saved nudge is cleared once on load. It existed to calibrate this by
    hand, and now that the measured value is the default, leaving it in place
    would apply on top and put the panel back where it started.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Stand the panel beside the character sheet, not inside it
    Three changes, all pulling the same way.
    The fill goes back to the dark it had before the header-band colour; that
    colour read as bulk rather than as background.
    The panel stops being tucked under the character frame's right border and
    sits a short gap clear of it, drawing in front rather than behind. Sharing
    a seam was meant to read as integration and instead read as one oversized
    frame. It is its own window standing beside the character sheet now, the
    way the guild information window does.
    The frame is no longer inflated by the border's inset, which was there to
    line its drawn edges up with the character frame's artwork - pointless
    once the two are deliberately separate, and it was making the panel bigger
    than the space it occupies. Top and bottom still track the artwork's, so
    the height matches the character sheet.
    Also clears out comments left behind by the borrowed-artwork attempts,
    which described anchoring that no longer exists.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Go back to the addon's own chrome for the panel
    Matching the character frame's border exactly was attempted at length and
    made things worse each time. Drawing its quadrants whole carried the
    paperdoll's slot recesses and the tab hardware into the middle of the
    panel. Slicing clean strips sampled the wrong half of the texture file,
    because those regions carry texcoords of their own that SetTexCoord
    discards. Drawing the edge by hand from measured colours lost the bevel.
    Back to ns.ApplyChrome - the same border the quick menu and the flyouts
    use - keeping the character frame's header-band colour as the fill, which
    was asked for separately and isn't part of what regressed. It reads as a
    sibling of the character frame rather than a continuation of it, and it
    doesn't degrade.
    Everything the detour did establish is kept: the measured position and
    size constants, the artwork padding, the scrollbar room, and the interior
    layout, which is unchanged - the panel is inflated by the border's inset
    so PANEL_WIDTH stays the width you actually see.
    Removes the slicing constants and the chrome diagnostic that described
    pieces which no longer exist.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Draw the panel's edge instead of slicing the frame's artwork
    Reusing the character frame's own textures never worked. Whole quadrants
    brought the paperdoll's slot recesses and the tab hardware with them.
    Slicing clean strips kept sampling the wrong part of the file - the
    right-hand quadrants are 128 wide drawn from the left half of a 256-wide
    texture, so those regions carry texcoords of their own and SetTexCoord
    discards that mapping. Composing with them still didn't land it.
    Measuring what the frames look like settled it instead. The character
    frame and the social window edge identically: a bright highlight outside
    at RGB ~141, a dark line immediately inside at ~20, over a mid tone that
    is the same colour as the header band the panel is already filled with.
    Three flat colours, so the panel draws them - exact, with no texture to
    sample wrongly and nothing left to drift.
    Removes the slicing machinery, its border-width tuning command, and the
    tests that covered it. /hg dock art still reports the frame's regions,
    now including their texcoords.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Slice inside the region's own texcoords
    This is why the border never came out right. Blizzard's right-hand frame
    quadrants are 128-wide regions drawn from the left half of a 256-wide
    file, so each region already carries texcoords of its own. Calling
    SetTexCoord with coordinates computed in whole-file space threw that
    mapping away and sampled the wrong half of the texture - which is exactly
    why drawing the quadrants whole looked right, and every sliced version
    since has looked wrong however the coordinates were adjusted.
    The slice now maps its fraction of the region onto the rectangle the
    region is actually showing. Every attempt to fix this by moving the sample
    • the x range, then the depth - was chasing the symptom.
      /hg dock art reports each region's texcoords, and two more cases cover
      composition and that a slice stays inside the half its region shows.
      Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Sample the border where it actually has a bevel
    /hg dock chrome confirmed all six pieces are shown and correctly placed,
    so the slices were never missing - they were sampling the wrong part of
    the artwork.
    The right border isn't uniform down its length. At depth 60, where this
    sampled, it's flat and dark; from about depth 120 down it carries the
    bevel that ends in the bright outer line at RGB 140,139,137. Verified the
    same at 125, 245 and 365, so the sample moves to 250. That dull dark band
    along the panel's edge was the frame's own artwork, just an atypical
    stretch of it.
    The fill also came back lighter than intended: 0.227 in, 71 on screen,
    against the header's 58. Scaled down to land on the header's rendered
    value rather than its nominal one.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Use the character frame's header band as the panel background
    The fill was a near-black of my own choosing. Sampling the character
    frame's header - the strip carrying the level and class - gives RGB
    58,53,49: warmer and considerably lighter. That's what the panel uses now.
    The sliced border still isn't showing. Its pieces are either absent or
    landing somewhere unhelpful, and from a screenshot those two look
    identical, so /hg dock chrome reports each piece's texture, whether it's
    shown, and the rectangle it occupies - against the panel's own rectangle.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Measure the artwork's edges from pixels instead of estimating
    Sampling the screenshot's pixels directly rather than eyeballing it: every
    constant describing where the artwork's visible edges sit was wrong, and
    each error had a visible symptom.
    At y=300 the character frame's bright edge is at x=443, the world shows
    through 444-453, and the panel's fill starts at 456 - a real gap. The
    artwork's right edge is 348, not 359, so the panel sat ten pixels clear of
    the frame and the right border strip sampled transparent padding, which is
    why only a single pixel of highlight drew instead of a bevel.
    At x=300 the character frame's top edge is at y=150 while the panel's is
    at 144: the artwork has five rows of transparent padding above its top
    border. The top and bottom strips were sampling from row zero, catching
    the padding and drawing nothing.
    At the bottom the frame's edge is y=681 against the panel's 656. The tabs
    overlap upward INTO the artwork, so its bottom is 11 below the tab row's
    top, not 10 above it - the sign was inverted, leaving the panel 21 short.
    Geometry tests updated to the measured values.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Fix the right border slice smearing across the panel
    The right-edge strip was anchored TOPRIGHT and BOTTOMRIGHT. Both points
    are on the same edge, so they fix its height and say nothing about its
    width - the texture fell back to its natural 128px and drew a stretched
    border smear a third of the way across the panel, which is the light band
    in the screenshot. The actual edges got no border at all.
    SetWidth on that one texture. The other five pieces anchor opposite
    corners or set an explicit size, so none of them had the same hole.
    The slice arithmetic is now Panel.TexCoords and covered by five cases in
    the geometry test: the two strips this uses, that their coordinates land
    inside the texture at all - sampling outside is how the paperdoll's slot
    recesses got in - and that depth in a bottom quadrant is measured from the
    artwork's top rather than the quadrant's.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Slice clean border strips instead of whole quadrants
    Drawing the character frame's quadrants wholesale got the style right but
    brought their contents along: the paperdoll's slot recesses, and along the
    bottom the hardware the character tabs bolt onto, which lands orphaned in
    the middle of a panel that has no tabs.
    Each edge is now a clean strip cut from the same textures with SetTexCoord
    • runs of border with nothing attached to them - and the bottom reuses the
      top border flipped, the top being the one horizontal edge with no hardware
      on it. The bottom-right corner mirrors the top-right for the same reason.
      Every piece anchors to the panel's own edges, which also drops the whole
      question of whether the panel and the artwork agree about where the
      frame's corner is: they no longer have to.
      /hg dock border re-slices at a different border thickness, so the one
      remaining guess - 11px - can be corrected in game rather than over another
      round of screenshots.
      Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Anchor the interior fill to the artwork, not the panel
    Zooming into the screenshot showed the fill and the borrowed artwork are
    not quite in the same place: a band of the paperdoll's slot recesses shows
    between the fill's edge and the border, on the right and along the bottom.
    The two were positioned from different things - the fill from the panel's
    bounds, the artwork from the derived visible corner - so any error in that
    corner separates them, and the gap fills with whatever the paperdoll had
    there. The fill is now placed from the artwork's own origin using the
    artwork's visible extents, so the two move together and the seam between
    them cannot open up regardless of where the panel itself lands.
    /hg dock also reports PaperDollFrame's origin and the computed artwork
    offset now, which is the number this all turns on.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Cover the paperdoll detail the borrowed artwork brings with it
    Drawing the character frame's own textures gets the border exactly right,
    but it also brings the paperdoll's interior along - the empty slot
    recesses show as a column of boxes down the panel's right side and another
    row across its bottom.
    The border is the only part worth borrowing, so everything inside it is
    covered with a flat fill. The left inset is zero: that edge is the
    character frame's own border, drawn by the character frame itself, so the
    fill runs right up to the seam.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Draw the character frame's own artwork on the panel
    /hg dock art settled what the border actually is: four texture quadrants
    laid out 384x512 on PaperDollFrame, the classic corner-piece frame. No
    reusable template exists for it, which is why both look-alikes tried here
    • the tan UI-DialogBox border, then the dark nine-slice - were visibly not
      it however they were sized or inset.
      So the panel stops approximating and draws those same four textures,
      read off PaperDollFrame at runtime rather than hardcoded, shifted right so
      their right border lands on the panel's right edge. The rest runs off to
      the left behind the character frame, which the panel already draws
      underneath. What shows is the character frame's own border, corners and
      interior simply continuing - there is no seam to match because there
      isn't one.
      That also removes the inflate-by-the-border-inset arithmetic from the last
      two rounds: the borrowed artwork carries the same transparent padding the
      character frame's does, so the panel's bounds are just the artwork's
      bounds again.
      Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Take the panel's top from the frame, not the close button
    The panel was sticking out above the character frame. Using the close
    button for the top was wrong: the artwork is flush with CharacterFrame's
    top, and measuring the close button pushed the panel up a dozen pixels -
    which also broke the height, since the artwork is 424 tall and that only
    comes out right when the top is the frame's own.
    The close button is still right for the horizontal, where /hg dock's 359
    matches the frame's real edge. Only the vertical reverts.
    /hg dock art now reports each region's size and anchor alongside its
    texture, since the character frame's artwork turns out to be four
    textures on PaperDollFrame rather than anything on CharacterFrame - the
    panel's border can be built from those same pieces once their layout is
    known.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Tuck the panel under the character frame's border
    Two fixes for "smack bang next to it" and "too short", both from the same
    cause as the last round: the panel's frame was being positioned correctly
    while its drawn border sat 11px inside those bounds.
    The frame is now inflated by that inset on every side, so the drawn edges
    land on the character frame's visible edges instead of inside them - which
    is what made the panel look short at top and bottom.
    Horizontally it goes one border width further, tucking the panel's left
    edge under the character frame's right border, and the panel now draws
    behind the character frame rather than in front. The seam between the
    two is therefore the character frame's own artwork, which matches by
    definition, instead of two borders in slightly different styles butted up
    against each other.
    The panel's other three borders still aren't the character frame's, so
    /hg dock art dumps that frame's own texture regions - the panel can then
    be built from the same pieces rather than a look-alike picked by eye.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Correct the border's drawn inset so the panel docks flush
    The position arithmetic was already right: /hg dock computes the visible
    corner at 359, and calibrating against the screenshot that is exactly
    where the character frame's right edge falls. The gap was the other half
    of the sum. The panel's frame sat at 355 but its border drew from ~366,
    so the two drawn edges were 7px apart however well the frame was placed.
    The border template draws its edge ~11px inside the frame's bounds, the
    same as the backdrop it replaced - not the ~4 assumed when the nine-slice
    path went in. One number, measured off the screenshot rather than guessed.
    Worth recording why this took two passes: a correct frame position with
    the wrong overlap is visually identical to a wrong frame position, and I
    kept re-deriving the position each time.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Take the panel's position from the close button
    /hg dock reported "no nine-slice", so the anchor added last commit never
    applied and the frame fell through to the slot-column derivation - which
    is wrong. Measured off the screenshot, the visible right edge is around
    352, but mirroring the left column's 21px margin onto the right derives
    364. The margins simply aren't symmetric; the right is nearer 9.
    The close button is a better reference and was available all along:
    Blizzard pins it to the artwork's top-right corner, so its own right and
    top edges sit a few pixels inside the frame's. That fixes the horizontal
    gap and the panel's top, which was sitting slightly above the character
    frame for the same reason - CharacterFrame's top is not the artwork's.
    The nine-slice path stays first in line for a client that has one, the
    column derivation drops to last, and /hg dock reports which is in use
    along with the corner it worked out.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Anchor the panel to the character frame's nine-slice
    The screenshot showed the panel still a few pixels clear of the frame and
    noticeably short at the bottom. Both come from the same mistake: every
    attempt so far reconstructed the visible frame's edges by measuring around
    CharacterFrame, whose own bounds sit in dead space.
    There is no need to reconstruct anything. The character frame is modern
    chrome now, so its border is a nine-slice, and CharacterFrame.NineSlice's
    bounds are the visible frame. Anchoring the panel's TOPLEFT and BOTTOMLEFT
    to its right edge matches all three edges at once - flush horizontally,
    and exactly as tall - with no arithmetic and no constants.
    The measured path is kept as a fallback for chrome without a nine-slice,
    and /hg dock now reports which one is in use.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Use the modern frame chrome, and keep the scrollbar inside the panel
    A screenshot settled what several rounds of guessing hadn't. Two things
    were wrong, and the first I had backwards.
    1.15.9 didn't only move Era's code onto the shared UI, it moved the look:
    the character frame is the dark nine-slice chrome now, not the old tan
    parchment. UI-DialogBox-Border was never going to match it at any edge
    size - it's the wrong frame entirely. Panels now use the same
    DialogBorderDarkTemplate nine-slice, probed for once at load, with the
    backdrop kept only as a fallback. ns.ApplyChrome is the one place that
    decides, and it also carries the overlap the border needs, which is much
    smaller for the nine-slice than for the backdrop.
    UIPanelScrollFrameTemplate anchors its scrollbar outside the scroll
    frame's right edge, so sizing a scroll frame to the full content width put
    the bar on the border or past it, outside the panel. Every list now gives
    up SCROLLBAR_ROOM for it and hides the bar when the list fits.
    The panel also spans the artwork's full height rather than insetting 12px
    top and bottom, so its edges line up with the character frame's.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Match the character frame's styling, and its height
    The panel's corners were squaring off because UI-DialogBox-Border was
    being drawn at edgeSize 16. That texture is cut for a 32px edge; below it
    the corner pieces get squashed and lose the chamfered "dulled point" the
    rest of the Classic UI has, which is enough to read as a different UI.
    Now drawn at Blizzard's own numbers - edgeSize 32, insets 11/12 - shared
    as ns.BACKDROP so the menu, the slot flyout and the options popout can't
    drift apart from it. Their interiors move in to clear the thicker border,
    and the docked panel widens to 214 to keep Equip and Save side by side.
    Height now comes from the frame rather than a constant. CharacterFrame
    runs past its artwork at the bottom too, to leave room for the tab row,
    and the tabs sit across the artwork's bottom edge - so they're what gets
    measured. The panel anchors top and bottom and comes out exactly as tall
    as the character frame, tracking it instead of guessing.
    Panel.ComputeArtBottom is separated out like the horizontal fit, with six
    cases covering the stock tab row, a moved frame, a missing tab row, and
    the fallback bounds. /hg dock reports the vertical numbers too.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Dock the panel from the measured artwork edge
    /hg dock on a 1.15.9 client reports CharacterFrame spanning 0..384 with
    the slot columns at 21..343, so the artwork ends around 364 and the frame
    carries ~20px of dead space past it - not the ~46 I had assumed from the
    vanilla UIPanelFrame convention. The fallback constant is corrected to
    match, and the geometry test now runs against those real numbers instead
    of my guess.
    Two fixes beyond the constant:
    The panel anchored its top to a slot button, which is most of the way down
    the frame, so it hung far below the character frame's bottom. Horizontal
    position still comes from the artwork's edge, but the anchor is now
    CharacterFrame's TOPLEFT with a measured x.
    Docking flush means overlapping: the backdrop's edge texture carries ~5px
    of transparent padding, so the panel's frame has to sit that much inside
    the artwork for the drawn edges to meet. That is now explicit rather than
    folded into a magic number.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Fix nil GetItemCooldown when opening a slot flyout
    1.15.9 removed the bare GetItemCooldown global, and C_Item carries no
    replacement in this build, so ns.API.GetItemCooldown's fallback chain
    resolved to nil. Nothing noticed until a flyout row tried to draw a
    cooldown swirl, which is any left-click on a slot while editing a set.
    Cooldowns are now read from where the item actually is - the bag slot it
    sits in, or the inventory slot it's worn in - which is information the
    caller always has and which uses calls that exist. The flyout threads the
    item's location through from the bag scan for that. An item the set points
    at but you aren't carrying simply has no cooldown, rather than an error.
    Two things to stop this recurring: ns.API no longer holds an entry that
    can't be satisfied on this client, and a login-time check reports any that
    turn up nil rather than waiting for a click to find them. Regression test
    covers cooldowns off a bag slot, off a worn slot, and off no location.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Edit set slots through the item flyout; add a dock nudge
    Slot editing was a single click cycling through worn / clear / ignore,
    which made you tour states you didn't want to reach the one you did, and
    gave no way to choose an item other than the one already on.
    Left-click on a slot now opens the same flyout used for swapping, in
    assign mode: it lists everything you're carrying that fits, leads with a
    "No item" row for a set that deliberately strips the slot, ticks whatever
    the set currently holds, and writes the choice into the set rather than
    onto the character. Gear the set points at that isn't carried - banked, or
    gone - is listed too, so it can be seen and replaced.
    Right-click takes the slot in or out of the set. Keeping that separate
    matters because "the set clears this slot" and "the set ignores this
    slot" are different things.
    Sets:CycleSlot is replaced by SetSlot and ToggleSlot, with the tests
    rewritten to match.
    Also fixes a real bug the linter caught in the new flyout code: local start, duration = baseID and API.GetItemCooldown(baseID) yields only the
    first return value, so no cooldown swirl would ever have drawn.
    The panel still isn't docking flush. Both attempts at deriving the
    artwork's edge have missed, so /hg dock now reports what the addon
    actually measures and /hg dock shifts it, saved per account.
    Meanwhile the panel hangs off the right slot column - the one reference
    point that's known good, since the button anchored to it lands correctly.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Fit the character panel and button to the frame's artwork
    CharacterFrame is a vanilla UIPanelFrame: 384x512 of frame holding
    338x424 of artwork. Its right edge is therefore ~46px outside the frame
    you can see, which is why the panel sat detached from it and why the
    button, anchored off the close button's own bounds, had no margin.
    Neither is anchored to the frame's bounds any more. Both are placed from
    the paperdoll's slot columns, which sit symmetrically inside the artwork:
    the left column's margin is also the right column's, so the right
    column's edge plus that margin gives the artwork's edge. The panel docks
    flush to it and the button sits above the top slot of the right-hand
    column, sharing its right edge and so inheriting the frame's own margin.
    Measured rather than hardcoded, so it also fits a frame another addon has
    resized, and re-measured on every PaperDollFrame show rather than once at
    login. A clamp falls back to the stock 46 if the numbers come out absurd.
    The arithmetic is Panel.ComputeArtInset, kept free of frame lookups so
    Tests/test_geometry.lua can exercise it: stock layout, moved frame,
    artwork that fills its frame, narrower padding, and the fallback cases.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Replace the sidebar tab with a button inside the character frame
    The spellbook sidebar-tab artwork is drawn to key into that frame's
    specific edge, so it looked wrong hanging off the character frame. In its
    place: a small action-button-styled toggle (icon plus the UI-Quickslot2
    ring at Blizzard's 1.83x ratio) sitting inside the frame, under the close
    button.
    The panel now hangs off that button rather than off the frame, so it
    reads as belonging to the control that opens it, and nothing measures
    against CharacterFrame's own bounds - which sit well outside the visible
    artwork.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Move set management onto the character sheet
    Replaces the standalone manager window with a panel docked to the
    character frame, opened by a sidebar tab, and makes the paperdoll itself
    the set editor.
    Docked outside rather than inside the frame: Classic's character frame
    has no right-hand column to borrow, so an inside panel would cover the
    model and the slot buttons you need to click while editing. The tab
    anchors off CharacterFrameCloseButton, which sits at the top-right of the
    artwork and so tracks it - CharacterFrame's own right edge is a long way
    outside the visible frame - and the panel anchors off the tab.
    Slot editing needs the overlays to own the mouse. A paperdoll slot's
    default handler picks the item up before any hook of ours sees the click,
    so there is nothing to cancel; instead each slot's overlay button takes
    the mouse only while a modifier is held or a set is being edited. The
    set's item is drawn on the overlay rather than by retexturing the slot,
    which PaperDollItemSlotButton_Update would undo on the next inventory
    event.
    The panel follows PaperDollFrame rather than its own parent, since
    switching to Reputation leaves CharacterFrame shown. That hide is not
    recorded as the user closing the panel.
    SlotMenus.lua becomes Paperdoll.lua and owns everything attached to the
    slot buttons; UI.lua becomes CharacterPanel.lua. Slot state and the edit
    cycle moved into Sets.lua so they can be tested headlessly - three new
    cases cover the cycle, bare slots, and the worn/stored distinction.
    Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
  • Initial commit: gear set manager for Classic Era
    Replaces ItemRack, which throws on client 1.15.9: it calls ShowHelm() and
    ShowCloak() unguarded in ItemRackEquip.lua, and both globals were removed
    when Era moved onto the modern shared UI codebase.
    Scope is sets, swapping and the character-sheet slot menus. ItemRack's
    auto-swap event rules and cooldown queues are deliberately left out.
    Notable design points:
    • Gear IDs are itemID:enchant:suffix:unique. The link's level field is
      dropped, which is what forced ItemRack to rewrite stored IDs on every
      ding. suffixID is never ignored, so random-suffix drops can't satisfy
      each other.
    • The swap engine only moves an item when it can find a strictly better
      match than what the slot already holds, so a set saved before an item
      was re-enchanted stops reporting that item missing.
    • Swaps run in passes that re-read the world each time rather than
      scripting a move order, driven by ITEM_LOCK_CHANGED plus a ticker -
      events alone deadlock, since a pass that can't move anything produces
      no events to wake up on.
    • The ItemRack import snapshots ItemRackUser into our own saved
      variables on first sight, so it keeps working once ItemRack is gone.
      It never writes to ItemRack's data.
      Tests/ holds headless harnesses (not in the TOC). The import is verified
      against real ItemRack SavedVariables files; the swap engine runs against
      a simulated character with worn slots, bags and an item cursor.
      Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com